diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000000..52ac3648b0
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,8 @@
+# These are supported funding model platforms
+
+github:
+patreon: ottowinter
+open_collective:
+ko_fi:
+tidelift:
+custom: https://esphome.io/guides/supporters.html
diff --git a/.gitignore b/.gitignore
index 6002612c13..b004947390 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,6 +75,7 @@ venv.bak/
 
 .pioenvs
 .piolibdeps
+.pio
 .vscode
 CMakeListsPrivate.txt
 CMakeLists.txt
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3278827486..3db0b982ae 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,7 +3,7 @@
 variables:
   DOCKER_DRIVER: overlay2
   DOCKER_HOST: tcp://docker:2375/
-  BASE_VERSION: '1.8.3'
+  BASE_VERSION: '2.0.1'
   TZ: UTC
 
 stages:
@@ -33,7 +33,7 @@ stages:
     - docker info
     - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD"
   script:
-    - docker run --rm --privileged hassioaddons/qemu-user-static:latest
+    - docker run --rm --privileged multiarch/qemu-user-static:4.1.0-1 --reset -p yes
     - TAG="${CI_COMMIT_TAG#v}"
     - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}"
     - echo "Tag ${TAG}"
@@ -107,10 +107,6 @@ lint-tidy:
   <<: *lint
   script:
     - pio init --ide atom
-    - |
-      if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then
-        patch -p0 < script/.neopixelbus.patch
-      fi
     - script/clang-tidy --all-headers --fix
     - script/ci-suggest-changes
 
diff --git a/.travis.yml b/.travis.yml
index dba512cb1f..aa848877c6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,30 +1,26 @@
 sudo: false
 language: python
-python: '2.7'
+python: '3.5'
 install: script/setup
 cache:
   directories:
     - "~/.platformio"
-    - "$TRAVIS_BUILD_DIR/.piolibdeps"
-    - "$TRAVIS_BUILD_DIR/tests/build/test1/.piolibdeps"
-    - "$TRAVIS_BUILD_DIR/tests/build/test2/.piolibdeps"
-    - "$TRAVIS_BUILD_DIR/tests/build/test3/.piolibdeps"
 
 matrix:
   fast_finish: true
   include:
-    - python: "2.7"
-      env: TARGET=Lint2.7
+    - python: "3.7"
+      env: TARGET=Lint3.7
       script:
         - script/ci-custom.py
         - flake8 esphome
         - pylint esphome
-    - python: "3.5.3"
-      env: TARGET=Lint3.5
+    - python: "3.5"
+      env: TARGET=Test3.5
       script:
-        - script/ci-custom.py
-        - flake8 esphome
-        - pylint esphome
+        - esphome tests/test1.yaml compile
+        - esphome tests/test2.yaml compile
+        - esphome tests/test3.yaml compile
     - python: "2.7"
       env: TARGET=Test2.7
       script:
@@ -44,10 +40,6 @@ matrix:
             - clang-format-7
       before_script:
         - pio init --ide atom
-        - |
-          if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then
-            patch -p0 < script/.neopixelbus.patch
-          fi
         - clang-tidy-7 -version
         - clang-format-7 -version
         - clang-apply-replacements-7 -version
diff --git a/MANIFEST.in b/MANIFEST.in
index f4a2cc672d..cdea2df2a6 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,6 @@
 include LICENSE
 include README.md
 include esphome/dashboard/templates/*.html
-recursive-include esphome/dashboard/static *.ico *.js *.css
+recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE
 recursive-include esphome *.cpp *.h *.tcc
+recursive-include esphome LICENSE.txt
diff --git a/docker/Dockerfile b/docker/Dockerfile
index a5edeeec00..11bbeeda2b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,8 +1,11 @@
-ARG BUILD_FROM=esphome/esphome-base-amd64:1.8.3
+ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.1
 FROM ${BUILD_FROM}
 
 COPY . .
-RUN pip2 install --no-cache-dir -e .
+RUN pip3 install --no-cache-dir -e .
+
+ENV USERNAME=""
+ENV PASSWORD=""
 
 WORKDIR /config
 ENTRYPOINT ["esphome"]
diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio
index 4d3d0b88f1..e5c9625680 100644
--- a/docker/Dockerfile.hassio
+++ b/docker/Dockerfile.hassio
@@ -6,7 +6,7 @@ COPY docker/rootfs/ /
 COPY setup.py setup.cfg MANIFEST.in /opt/esphome/
 COPY esphome /opt/esphome/esphome
 
-RUN pip2 install --no-cache-dir -e /opt/esphome
+RUN pip3 install --no-cache-dir -e /opt/esphome
 
 # Build arguments
 ARG BUILD_VERSION=dev
diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint
index 2cc65b85d2..5d8893bdbe 100644
--- a/docker/Dockerfile.lint
+++ b/docker/Dockerfile.lint
@@ -1,4 +1,4 @@
-FROM esphome/esphome-base-amd64:1.8.3
+FROM esphome/esphome-base-amd64:2.0.1
 
 RUN \
     apt-get update \
@@ -12,7 +12,9 @@ RUN \
         /var/lib/apt/lists/*
 
 COPY requirements_test.txt /requirements_test.txt
-RUN pip2 install --no-cache-dir wheel && pip2 install --no-cache-dir -r /requirements_test.txt
+RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt
+
+RUN ln -s /usr/bin/pip3 /usr/bin/pip && ln -f -s /usr/bin/python3 /usr/bin/python
 
 VOLUME ["/esphome"]
 WORKDIR /esphome
diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh
index 8fc1b472c0..086c5af19f 100644
--- a/docker/rootfs/etc/cont-init.d/30-esphome.sh
+++ b/docker/rootfs/etc/cont-init.d/30-esphome.sh
@@ -10,6 +10,6 @@ if bashio::config.has_value 'esphome_version'; then
     esphome_version=$(bashio::config 'esphome_version')
     full_url="https://github.com/esphome/esphome/archive/${esphome_version}.zip"
     bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..."
-    pip2 install -U --no-cache-dir "${full_url}" \
+    pip3 install -U --no-cache-dir "${full_url}" \
       || bashio::exit.nok "Failed installing esphome pinned version."
 fi
diff --git a/esphome/__main__.py b/esphome/__main__.py
index c48d49b9ac..a78413ff04 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -11,11 +11,11 @@ from esphome import const, writer, yaml_util
 import esphome.codegen as cg
 from esphome.config import iter_components, read_config, strip_default_ids
 from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \
-    CONF_PASSWORD, CONF_PORT
+    CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS
 from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority
 from esphome.helpers import color, indent
 from esphome.py_compat import IS_PY2, safe_input
-from esphome.util import run_external_command, run_external_process, safe_print
+from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -24,7 +24,7 @@ def get_serial_ports():
     # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py
     from serial.tools.list_ports import comports
     result = []
-    for port, desc, info in comports():
+    for port, desc, info in comports(include_links=True):
         if not port:
             continue
         if "VID:PID" in info:
@@ -35,7 +35,9 @@ def get_serial_ports():
 
 def choose_prompt(options):
     if not options:
-        raise ValueError
+        raise EsphomeError("Found no valid options for upload/logging, please make sure relevant "
+                           "sections (ota, mqtt, ...) are in your configuration and/or the device "
+                           "is plugged in.")
 
     if len(options) == 1:
         return options[0][1]
@@ -130,6 +132,7 @@ def wrap_to_code(name, comp):
             conf_str = yaml_util.dump(conf)
             if IS_PY2:
                 conf_str = conf_str.decode('utf-8')
+            conf_str = conf_str.replace('//', '')
             cg.add(cg.LineComment(indent(conf_str)))
         yield coro(conf)
 
@@ -157,12 +160,13 @@ def compile_program(args, config):
     from esphome import platformio_api
 
     _LOGGER.info("Compiling app...")
-    return platformio_api.run_compile(config, args.verbose)
+    return platformio_api.run_compile(config, CORE.verbose)
 
 
 def upload_using_esptool(config, port):
     path = CORE.firmware_bin
     cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset',
+           '--baud', str(config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 460800)),
            '--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path]
 
     if os.environ.get('ESPHOME_USE_SUBPROCESS') is None:
@@ -180,15 +184,14 @@ def upload_program(config, args, host):
 
         if CORE.is_esp8266:
             return upload_using_esptool(config, host)
-        return platformio_api.run_upload(config, args.verbose, host)
+        return platformio_api.run_upload(config, CORE.verbose, host)
 
     from esphome import espota2
 
     ota_conf = config[CONF_OTA]
     remote_port = ota_conf[CONF_PORT]
     password = ota_conf[CONF_PASSWORD]
-    res = espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
-    return res
+    return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
 
 
 def show_logs(config, args, port):
@@ -218,6 +221,7 @@ def clean_mqtt(config, args):
 def setup_log(debug=False, quiet=False):
     if debug:
         log_level = logging.DEBUG
+        CORE.verbose = True
     elif quiet:
         log_level = logging.CRITICAL
     else:
@@ -250,12 +254,12 @@ def setup_log(debug=False, quiet=False):
 def command_wizard(args):
     from esphome import wizard
 
-    return wizard.wizard(args.configuration)
+    return wizard.wizard(args.configuration[0])
 
 
 def command_config(args, config):
     _LOGGER.info("Configuration is valid!")
-    if not args.verbose:
+    if not CORE.verbose:
         config = strip_default_ids(config)
     safe_print(yaml_util.dump(config))
     return 0
@@ -264,7 +268,7 @@ def command_config(args, config):
 def command_vscode(args):
     from esphome import vscode
 
-    CORE.config_path = args.configuration
+    CORE.config_path = args.configuration[0]
     vscode.read_config(args)
 
 
@@ -350,11 +354,52 @@ def command_dashboard(args):
     return dashboard.start_web_server(args)
 
 
+def command_update_all(args):
+    import click
+
+    success = {}
+    files = list_yaml_files(args.configuration[0])
+    twidth = 60
+
+    def print_bar(middle_text):
+        middle_text = " {} ".format(middle_text)
+        width = len(click.unstyle(middle_text))
+        half_line = "=" * ((twidth - width) // 2)
+        click.echo("%s%s%s" % (half_line, middle_text, half_line))
+
+    for f in files:
+        print("Updating {}".format(color('cyan', f)))
+        print('-' * twidth)
+        print()
+        rc = run_external_process('esphome', '--dashboard', f, 'run', '--no-logs')
+        if rc == 0:
+            print_bar("[{}] {}".format(color('bold_green', 'SUCCESS'), f))
+            success[f] = True
+        else:
+            print_bar("[{}] {}".format(color('bold_red', 'ERROR'), f))
+            success[f] = False
+
+        print()
+        print()
+        print()
+
+    print_bar('[{}]'.format(color('bold_white', 'SUMMARY')))
+    failed = 0
+    for f in files:
+        if success[f]:
+            print("  - {}: {}".format(f, color('green', 'SUCCESS')))
+        else:
+            print("  - {}: {}".format(f, color('bold_red', 'FAILED')))
+            failed += 1
+    return failed
+
+
 PRE_CONFIG_ACTIONS = {
     'wizard': command_wizard,
     'version': command_version,
     'dashboard': command_dashboard,
     'vscode': command_vscode,
+    'update-all': command_update_all,
 }
 
 POST_CONFIG_ACTIONS = {
@@ -370,13 +415,13 @@ POST_CONFIG_ACTIONS = {
 
 
 def parse_args(argv):
-    parser = argparse.ArgumentParser(prog='esphome')
+    parser = argparse.ArgumentParser(description='ESPHome v{}'.format(const.__version__))
     parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.",
                         action='store_true')
     parser.add_argument('-q', '--quiet', help="Disable all esphome logs.",
                         action='store_true')
     parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true')
-    parser.add_argument('configuration', help='Your YAML configuration file.')
+    parser.add_argument('configuration', help='Your YAML configuration file.', nargs='*')
 
     subparsers = parser.add_subparsers(help='Commands', dest='command')
     subparsers.required = True
@@ -433,7 +478,11 @@ def parse_args(argv):
                                       help="Create a simple web server for a dashboard.")
     dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.",
                            type=int, default=6052)
-    dashboard.add_argument("--password", help="The optional password to require for all requests.",
+    dashboard.add_argument("--username", help="The optional username to require "
+                                              "for authentication.",
+                           type=str, default='')
+    dashboard.add_argument("--password", help="The optional password to require "
+                                              "for authentication.",
                            type=str, default='')
     dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.",
                            action='store_true')
@@ -446,6 +495,8 @@ def parse_args(argv):
     vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS)
     vscode.add_argument('--ace', action='store_true')
 
+    subparsers.add_parser('update-all', help=argparse.SUPPRESS)
+
     return parser.parse_args(argv[1:])
 
 
@@ -454,6 +505,15 @@ def run_esphome(argv):
     CORE.dashboard = args.dashboard
 
     setup_log(args.verbose, args.quiet)
+    if args.command != 'version' and not args.configuration:
+        _LOGGER.error("Missing configuration parameter, see esphome --help.")
+        return 1
+
+    if IS_PY2:
+        _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated "
+                        "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 "
+                        "or higher.")
+
     if args.command in PRE_CONFIG_ACTIONS:
         try:
             return PRE_CONFIG_ACTIONS[args.command](args)
@@ -461,21 +521,28 @@ def run_esphome(argv):
             _LOGGER.error(e)
             return 1
 
-    CORE.config_path = args.configuration
+    for conf_path in args.configuration:
+        CORE.config_path = conf_path
+        CORE.dashboard = args.dashboard
 
-    config = read_config(args.verbose)
-    if config is None:
-        return 1
-    CORE.config = config
+        config = read_config()
+        if config is None:
+            return 1
+        CORE.config = config
+
+        if args.command not in POST_CONFIG_ACTIONS:
+            safe_print(u"Unknown command {}".format(args.command))
 
-    if args.command in POST_CONFIG_ACTIONS:
         try:
-            return POST_CONFIG_ACTIONS[args.command](args, config)
+            rc = POST_CONFIG_ACTIONS[args.command](args, config)
         except EsphomeError as e:
             _LOGGER.error(e)
             return 1
-    safe_print(u"Unknown command {}".format(args.command))
-    return 1
+        if rc != 0:
+            return rc
+
+        CORE.reset()
+    return 0
 
 
 def main():
diff --git a/esphome/api/api.proto b/esphome/api/api.proto
deleted file mode 100644
index 37cb792959..0000000000
--- a/esphome/api/api.proto
+++ /dev/null
@@ -1,330 +0,0 @@
-syntax = "proto3";
-
-// The Home Assistant protocol is structured as a simple
-// TCP socket with short binary messages encoded in the protocol buffers format
-// First, a message in this protocol has a specific format:
-//  * VarInt denoting the size of the message object. (type is not part of this)
-//  * VarInt denoting the type of message.
-//  * The message object encoded as a ProtoBuf message
-
-// The connection is established in 4 steps:
-//  * First, the client connects to the server and sends a "Hello Request" identifying itself
-//  * The server responds with a "Hello Response" and selects the protocol version
-//  * After receiving this message, the client attempts to authenticate itself using
-//    the password and a "Connect Request"
-//  * The server responds with a "Connect Response" and notifies of invalid password.
-// If anything in this initial process fails, the connection must immediately closed
-// by both sides and _no_ disconnection message is to be sent.
-
-// Message sent at the beginning of each connection
-// Can only be sent by the client and only at the beginning of the connection
-message HelloRequest {
-  // Description of client (like User Agent)
-  // For example "Home Assistant"
-  // Not strictly necessary to send but nice for debugging
-  // purposes.
-  string client_info = 1;
-}
-
-// Confirmation of successful connection request.
-// Can only be sent by the server and only at the beginning of the connection
-message HelloResponse {
-  // The version of the API to use. The _client_ (for example Home Assistant) needs to check
-  // for compatibility and if necessary adopt to an older API.
-  // Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_
-  // Minor is for breaking changes in individual messages - a mismatch will lead to a warning message
-  uint32 api_version_major = 1;
-  uint32 api_version_minor = 2;
-
-  // A string identifying the server (ESP); like client info this may be empty
-  // and only exists for debugging/logging purposes.
-  // For example "ESPHome v1.10.0 on ESP8266"
-  string server_info = 3;
-}
-
-// Message sent at the beginning of each connection to authenticate the client
-// Can only be sent by the client and only at the beginning of the connection
-message ConnectRequest {
-  // The password to log in with
-  string password = 1;
-}
-
-// Confirmation of successful connection. After this the connection is available for all traffic.
-// Can only be sent by the server and only at the beginning of the connection
-message ConnectResponse {
-  bool invalid_password = 1;
-}
-
-// Request to close the connection.
-// Can be sent by both the client and server
-message DisconnectRequest {
-  // Do not close the connection before the acknowledgement arrives
-}
-
-message DisconnectResponse {
-  // Empty - Both parties are required to close the connection after this
-  // message has been received.
-}
-
-message PingRequest {
-  // Empty
-}
-
-message PingResponse {
-  // Empty
-}
-
-message DeviceInfoRequest {
-  // Empty
-}
-
-message DeviceInfoResponse {
-  bool uses_password = 1;
-
-  // The name of the node, given by "App.set_name()"
-  string name = 2;
-
-  // The mac address of the device. For example "AC:BC:32:89:0E:A9"
-  string mac_address = 3;
-
-  // A string describing the ESPHome version. For example "1.10.0"
-  string esphome_core_version = 4;
-
-  // A string describing the date of compilation, this is generated by the compiler
-  // and therefore may not be in the same format all the time.
-  // If the user isn't using esphome, this will also not be set.
-  string compilation_time = 5;
-
-  // The model of the board. For example NodeMCU
-  string model = 6;
-
-  bool has_deep_sleep = 7;
-}
-
-message ListEntitiesRequest {
-  // Empty
-}
-
-message ListEntitiesBinarySensorResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  string device_class = 5;
-  bool is_status_binary_sensor = 6;
-}
-message ListEntitiesCoverResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  bool is_optimistic = 5;
-}
-message ListEntitiesFanResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  bool supports_oscillation = 5;
-  bool supports_speed = 6;
-}
-message ListEntitiesLightResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  bool supports_brightness = 5;
-  bool supports_rgb = 6;
-  bool supports_white_value = 7;
-  bool supports_color_temperature = 8;
-  float min_mireds = 9;
-  float max_mireds = 10;
-  repeated string effects = 11;
-}
-message ListEntitiesSensorResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  string icon = 5;
-  string unit_of_measurement = 6;
-  int32 accuracy_decimals = 7;
-}
-message ListEntitiesSwitchResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  string icon = 5;
-  bool optimistic = 6;
-}
-message ListEntitiesTextSensorResponse {
-  string object_id = 1;
-  fixed32 key = 2;
-  string name = 3;
-  string unique_id = 4;
-
-  string icon = 5;
-}
-message ListEntitiesDoneResponse {
-  // Empty
-}
-
-message SubscribeStatesRequest {
-  // Empty
-}
-message BinarySensorStateResponse {
-  fixed32 key = 1;
-  bool state = 2;
-}
-message CoverStateResponse {
-  fixed32 key = 1;
-  enum CoverState {
-    OPEN = 0;
-    CLOSED = 1;
-  }
-  CoverState state = 2;
-}
-enum FanSpeed {
-  LOW = 0;
-  MEDIUM = 1;
-  HIGH = 2;
-}
-message FanStateResponse {
-  fixed32 key = 1;
-  bool state = 2;
-  bool oscillating = 3;
-  FanSpeed speed = 4;
-}
-message LightStateResponse {
-  fixed32 key = 1;
-  bool state = 2;
-  float brightness = 3;
-  float red = 4;
-  float green = 5;
-  float blue = 6;
-  float white = 7;
-  float color_temperature = 8;
-  string effect = 9;
-}
-message SensorStateResponse {
-  fixed32 key = 1;
-  float state = 2;
-}
-message SwitchStateResponse {
-  fixed32 key = 1;
-  bool state = 2;
-}
-message TextSensorStateResponse {
-  fixed32 key = 1;
-  string state = 2;
-}
-
-message CoverCommandRequest {
-  fixed32 key = 1;
-  enum CoverCommand {
-    OPEN = 0;
-    CLOSE = 1;
-    STOP = 2;
-  }
-  bool has_state = 2;
-  CoverCommand command = 3;
-}
-message FanCommandRequest {
-  fixed32 key = 1;
-  bool has_state = 2;
-  bool state = 3;
-  bool has_speed = 4;
-  FanSpeed speed = 5;
-  bool has_oscillating = 6;
-  bool oscillating = 7;
-}
-message LightCommandRequest {
-  fixed32 key = 1;
-  bool has_state = 2;
-  bool state = 3;
-  bool has_brightness = 4;
-  float brightness = 5;
-  bool has_rgb = 6;
-  float red = 7;
-  float green = 8;
-  float blue = 9;
-  bool has_white = 10;
-  float white = 11;
-  bool has_color_temperature = 12;
-  float color_temperature = 13;
-  bool has_transition_length = 14;
-  uint32 transition_length = 15;
-  bool has_flash_length = 16;
-  uint32 flash_length = 17;
-  bool has_effect = 18;
-  string effect = 19;
-}
-message SwitchCommandRequest {
-  fixed32 key = 1;
-  bool state = 2;
-}
-
-enum LogLevel {
-  NONE = 0;
-  ERROR = 1;
-  WARN = 2;
-  INFO = 3;
-  DEBUG = 4;
-  VERBOSE = 5;
-  VERY_VERBOSE = 6;
-}
-
-message SubscribeLogsRequest {
-  LogLevel level = 1;
-  bool dump_config = 2;
-}
-
-message SubscribeLogsResponse {
-  LogLevel level = 1;
-  string tag = 2;
-  string message = 3;
-  bool send_failed = 4;
-}
-
-message SubscribeServiceCallsRequest {
-
-}
-
-message ServiceCallResponse {
-  string service = 1;
-  map<string, string> data = 2;
-  map<string, string> data_template = 3;
-  map<string, string> variables = 4;
-}
-
-// 1. Client sends SubscribeHomeAssistantStatesRequest
-// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async)
-// 3. Client sends HomeAssistantStateResponse for state changes.
-message SubscribeHomeAssistantStatesRequest {
-
-}
-
-message SubscribeHomeAssistantStateResponse {
-  string entity_id = 1;
-}
-
-message HomeAssistantStateResponse {
-  string entity_id = 1;
-  string state = 2;
-}
-
-message GetTimeRequest {
-
-}
-
-message GetTimeResponse {
-  fixed32 epoch_seconds = 1;
-}
-
diff --git a/esphome/api/client.py b/esphome/api/client.py
index cdafcc1d74..0c52674287 100644
--- a/esphome/api/client.py
+++ b/esphome/api/client.py
@@ -14,7 +14,7 @@ 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, color
-from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte, format_bytes
+from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte
 from esphome.util import safe_print
 
 _LOGGER = logging.getLogger(__name__)
@@ -247,7 +247,7 @@ class APIClient(threading.Thread):
         if self._socket is None:
             raise APIConnectionError("Socket closed")
 
-        _LOGGER.debug("Write: %s", format_bytes(data))
+        # _LOGGER.debug("Write: %s", format_bytes(data))
         with self._socket_write_lock:
             try:
                 self._socket.sendall(data)
@@ -275,7 +275,7 @@ class APIClient(threading.Thread):
         req += encoded
         self._write(req)
 
-    def _send_message_await_response_complex(self, send_msg, do_append, do_stop, timeout=1):
+    def _send_message_await_response_complex(self, send_msg, do_append, do_stop, timeout=5):
         event = threading.Event()
         responses = []
 
@@ -296,7 +296,7 @@ class APIClient(threading.Thread):
             raise APIConnectionError("Timeout while waiting for message response!")
         return responses
 
-    def _send_message_await_response(self, send_msg, response_type, timeout=1):
+    def _send_message_await_response(self, send_msg, response_type, timeout=5):
         def is_response(msg):
             return isinstance(msg, response_type)
 
@@ -321,13 +321,13 @@ class APIClient(threading.Thread):
         self._close_socket()
 
         if self.on_disconnect is not None and on_disconnect:
-            self.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=None, dump_config=False):
+    def subscribe_logs(self, on_log, log_level=7, dump_config=False):
         self._check_authenticated()
 
         def on_msg(msg):
@@ -336,8 +336,7 @@ class APIClient(threading.Thread):
 
         self._message_handlers.append(on_msg)
         req = pb.SubscribeLogsRequest(dump_config=dump_config)
-        if log_level is not None:
-            req.level = log_level
+        req.level = log_level
         self._send_message(req)
 
     def _recv(self, amount):
@@ -411,7 +410,7 @@ class APIClient(threading.Thread):
                 self._socket = None
             self._connected = False
             if self.on_disconnect is not None:
-                self.on_disconnect()
+                self.on_disconnect(None)
         elif isinstance(msg, pb.PingRequest):
             self._send_message(pb.PingResponse())
         elif isinstance(msg, pb.GetTimeRequest):
@@ -453,7 +452,7 @@ def run_logs(config, address):
             _LOGGER.info("Successfully connected to %s", address)
             return
 
-        wait_time = min(2**tries, 300)
+        wait_time = int(min(1.5**min(tries, 100), 30))
         if not has_connects:
             _LOGGER.warning(u"Initial connection failed. The ESP might not be connected "
                             u"to WiFi yet (%s). Re-Trying in %s seconds",
diff --git a/esphome/components/ade7953/__init__.py b/esphome/components/ade7953/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp
new file mode 100644
index 0000000000..9316d9cad0
--- /dev/null
+++ b/esphome/components/ade7953/ade7953.cpp
@@ -0,0 +1,51 @@
+#include "ade7953.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace ade7953 {
+
+static const char *TAG = "ade7953";
+
+void ADE7953::dump_config() {
+  ESP_LOGCONFIG(TAG, "ADE7953:");
+  LOG_I2C_DEVICE(this);
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "Voltage Sensor", this->voltage_sensor_);
+  LOG_SENSOR("  ", "Current A Sensor", this->current_a_sensor_);
+  LOG_SENSOR("  ", "Current B Sensor", this->current_b_sensor_);
+  LOG_SENSOR("  ", "Active Power A Sensor", this->active_power_a_sensor_);
+  LOG_SENSOR("  ", "Active Power B Sensor", this->active_power_b_sensor_);
+}
+
+#define ADE_PUBLISH_(name, factor) \
+  if (name) { \
+    float value = *name / factor; \
+    this->name##_sensor_->publish_state(value); \
+  }
+#define ADE_PUBLISH(name, factor) ADE_PUBLISH_(name, factor)
+
+void ADE7953::update() {
+  if (!this->is_setup_)
+    return;
+
+  auto active_power_a = this->ade_read_<int32_t>(0x0312);
+  ADE_PUBLISH(active_power_a, 154.0f);
+  auto active_power_b = this->ade_read_<int32_t>(0x0313);
+  ADE_PUBLISH(active_power_b, 154.0f);
+  auto current_a = this->ade_read_<uint32_t>(0x031A);
+  ADE_PUBLISH(current_a, 100000.0f);
+  auto current_b = this->ade_read_<uint32_t>(0x031B);
+  ADE_PUBLISH(current_b, 100000.0f);
+  auto voltage = this->ade_read_<uint32_t>(0x031C);
+  ADE_PUBLISH(voltage, 26000.0f);
+
+  //    auto apparent_power_a = this->ade_read_<int32_t>(0x0310);
+  //    auto apparent_power_b = this->ade_read_<int32_t>(0x0311);
+  //    auto reactive_power_a = this->ade_read_<int32_t>(0x0314);
+  //    auto reactive_power_b = this->ade_read_<int32_t>(0x0315);
+  //    auto power_factor_a = this->ade_read_<int16_t>(0x010A);
+  //    auto power_factor_b = this->ade_read_<int16_t>(0x010B);
+}
+
+}  // namespace ade7953
+}  // namespace esphome
diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h
new file mode 100644
index 0000000000..7591bc1684
--- /dev/null
+++ b/esphome/components/ade7953/ade7953.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace ade7953 {
+
+class ADE7953 : public i2c::I2CDevice, public PollingComponent {
+ public:
+  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
+  void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; }
+  void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; }
+  void set_active_power_a_sensor(sensor::Sensor *active_power_a_sensor) {
+    active_power_a_sensor_ = active_power_a_sensor;
+  }
+  void set_active_power_b_sensor(sensor::Sensor *active_power_b_sensor) {
+    active_power_b_sensor_ = active_power_b_sensor;
+  }
+
+  void setup() override {
+    this->set_timeout(100, [this]() {
+      this->ade_write_<uint8_t>(0x0010, 0x04);
+      this->ade_write_<uint8_t>(0x00FE, 0xAD);
+      this->ade_write_<uint16_t>(0x0120, 0x0030);
+      this->is_setup_ = true;
+    });
+  }
+
+  void dump_config() override;
+
+  void update() override;
+
+ protected:
+  template<typename T> bool ade_write_(uint16_t reg, T value) {
+    std::vector<uint8_t> data;
+    data.push_back(reg >> 8);
+    data.push_back(reg >> 0);
+    for (int i = sizeof(T) - 1; i >= 0; i--)
+      data.push_back(value >> (i * 8));
+    return this->write_bytes_raw(data);
+  }
+  template<typename T> optional<T> ade_read_(uint16_t reg) {
+    uint8_t hi = reg >> 8;
+    uint8_t lo = reg >> 0;
+    if (!this->write_bytes_raw({hi, lo}))
+      return {};
+    auto ret = this->read_bytes_raw<sizeof(T)>();
+    if (!ret.has_value())
+      return {};
+    T result = 0;
+    for (int i = 0, j = sizeof(T) - 1; i < sizeof(T); i++, j--)
+      result |= T((*ret)[i]) << (j * 8);
+    return result;
+  }
+
+  bool is_setup_{false};
+  sensor::Sensor *voltage_sensor_{nullptr};
+  sensor::Sensor *current_a_sensor_{nullptr};
+  sensor::Sensor *current_b_sensor_{nullptr};
+  sensor::Sensor *active_power_a_sensor_{nullptr};
+  sensor::Sensor *active_power_b_sensor_{nullptr};
+};
+
+}  // namespace ade7953
+}  // namespace esphome
diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py
new file mode 100644
index 0000000000..4fcd307332
--- /dev/null
+++ b/esphome/components/ade7953/sensor.py
@@ -0,0 +1,39 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, i2c
+from esphome.const import CONF_ID, CONF_VOLTAGE, \
+    UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT
+
+DEPENDENCIES = ['i2c']
+
+ace7953_ns = cg.esphome_ns.namespace('ade7953')
+ADE7953 = ace7953_ns.class_('ADE7953', cg.PollingComponent, i2c.I2CDevice)
+
+CONF_CURRENT_A = 'current_a'
+CONF_CURRENT_B = 'current_b'
+CONF_ACTIVE_POWER_A = 'active_power_a'
+CONF_ACTIVE_POWER_B = 'active_power_b'
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(ADE7953),
+
+    cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1),
+    cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2),
+    cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2),
+    cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1),
+    cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1),
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+
+    for key in [CONF_VOLTAGE, CONF_CURRENT_A, CONF_CURRENT_B, CONF_ACTIVE_POWER_A,
+                CONF_ACTIVE_POWER_B]:
+        if key not in config:
+            continue
+        conf = config[key]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(getattr(var, 'set_{}_sensor'.format(key))(sens))
diff --git a/esphome/components/ads1115/__init__.py b/esphome/components/ads1115/__init__.py
index 28cfac49ec..a41a521ba7 100644
--- a/esphome/components/ads1115/__init__.py
+++ b/esphome/components/ads1115/__init__.py
@@ -10,8 +10,10 @@ MULTI_CONF = True
 ads1115_ns = cg.esphome_ns.namespace('ads1115')
 ADS1115Component = ads1115_ns.class_('ADS1115Component', cg.Component, i2c.I2CDevice)
 
+CONF_CONTINUOUS_MODE = 'continuous_mode'
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.declare_id(ADS1115Component),
+    cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean,
 }).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(None))
 
 
@@ -19,3 +21,5 @@ def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     yield cg.register_component(var, config)
     yield i2c.register_i2c_device(var, config)
+
+    cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE]))
diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp
index 0d82fce533..0899571a47 100644
--- a/esphome/components/ads1115/ads1115.cpp
+++ b/esphome/components/ads1115/ads1115.cpp
@@ -29,9 +29,15 @@ void ADS1115Component::setup() {
   //        0bxxxx000xxxxxxxxx
   config |= ADS1115_GAIN_6P144 << 9;
 
-  // Set singleshot mode
-  //        0bxxxxxxx1xxxxxxxx
-  config |= 0b0000000100000000;
+  if (this->continuous_mode_) {
+    // Set continuous mode
+    //        0bxxxxxxx0xxxxxxxx
+    config |= 0b0000000000000000;
+  } else {
+    // Set singleshot mode
+    //        0bxxxxxxx1xxxxxxxx
+    config |= 0b0000000100000000;
+  }
 
   // Set data rate - 860 samples per second (we're in singleshot mode)
   //        0bxxxxxxxx100xxxxx
@@ -57,6 +63,8 @@ void ADS1115Component::setup() {
     this->mark_failed();
     return;
   }
+  this->prev_config_ = config;
+
   for (auto *sensor : this->sensors_) {
     this->set_interval(sensor->get_name(), sensor->update_interval(),
                        [this, sensor] { this->request_measurement(sensor); });
@@ -75,13 +83,8 @@ void ADS1115Component::dump_config() {
     ESP_LOGCONFIG(TAG, "    Gain: %u", sensor->get_gain());
   }
 }
-float ADS1115Component::get_setup_priority() const { return setup_priority::DATA; }
 float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
-  uint16_t config;
-  if (!this->read_byte_16(ADS1115_REGISTER_CONFIG, &config)) {
-    this->status_set_warning();
-    return NAN;
-  }
+  uint16_t config = this->prev_config_;
   // Multiplexer
   //        0bxBBBxxxxxxxxxxxx
   config &= 0b1000111111111111;
@@ -91,25 +94,31 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
   //        0bxxxxBBBxxxxxxxxx
   config &= 0b1111000111111111;
   config |= (sensor->get_gain() & 0b111) << 9;
-  // Start conversion
-  config |= 0b1000000000000000;
 
-  if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
-    this->status_set_warning();
-    return NAN;
+  if (!this->continuous_mode_) {
+    // Start conversion
+    config |= 0b1000000000000000;
   }
 
-  // about 1.6 ms with 860 samples per second
-  delay(2);
-
-  uint32_t start = millis();
-  while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) {
-    if (millis() - start > 100) {
-      ESP_LOGW(TAG, "Reading ADS1115 timed out");
+  if (!this->continuous_mode_ || this->prev_config_ != config) {
+    if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
       this->status_set_warning();
       return NAN;
     }
-    yield();
+    this->prev_config_ = config;
+
+    // about 1.6 ms with 860 samples per second
+    delay(2);
+
+    uint32_t start = millis();
+    while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) {
+      if (millis() - start > 100) {
+        ESP_LOGW(TAG, "Reading ADS1115 timed out");
+        this->status_set_warning();
+        return NAN;
+      }
+      yield();
+    }
   }
 
   uint16_t raw_conversion;
@@ -147,10 +156,6 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
   return millivolts / 1e3f;
 }
 
-uint8_t ADS1115Sensor::get_multiplexer() const { return this->multiplexer_; }
-void ADS1115Sensor::set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
-uint8_t ADS1115Sensor::get_gain() const { return this->gain_; }
-void ADS1115Sensor::set_gain(ADS1115Gain gain) { this->gain_ = gain; }
 float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); }
 void ADS1115Sensor::update() {
   float v = this->parent_->request_measurement(this);
diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h
index 966541ef4f..059436d142 100644
--- a/esphome/components/ads1115/ads1115.h
+++ b/esphome/components/ads1115/ads1115.h
@@ -37,13 +37,16 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
   void setup() override;
   void dump_config() override;
   /// HARDWARE_LATE setup priority
-  float get_setup_priority() const override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
 
   /// Helper method to request a measurement from a sensor.
   float request_measurement(ADS1115Sensor *sensor);
 
  protected:
   std::vector<ADS1115Sensor *> sensors_;
+  uint16_t prev_config_{0};
+  bool continuous_mode_;
 };
 
 /// Internal holder class that is in instance of Sensor so that the hub can create individual sensors.
@@ -51,12 +54,12 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol
  public:
   ADS1115Sensor(ADS1115Component *parent) : parent_(parent) {}
   void update() override;
-  void set_multiplexer(ADS1115Multiplexer multiplexer);
-  void set_gain(ADS1115Gain gain);
+  void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; }
+  void set_gain(ADS1115Gain gain) { gain_ = gain; }
 
   float sample() override;
-  uint8_t get_multiplexer() const;
-  uint8_t get_gain() const;
+  uint8_t get_multiplexer() const { return multiplexer_; }
+  uint8_t get_gain() const { return gain_; }
 
  protected:
   ADS1115Component *parent_;
diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py
index 5c15c68db7..43c7d71e74 100644
--- a/esphome/components/api/__init__.py
+++ b/esphome/components/api/__init__.py
@@ -3,44 +3,40 @@ import esphome.config_validation as cv
 from esphome import automation
 from esphome.automation import Condition
 from esphome.const import CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, CONF_PASSWORD, CONF_PORT, \
-    CONF_REBOOT_TIMEOUT, CONF_SERVICE, CONF_VARIABLES, CONF_SERVICES, CONF_TRIGGER_ID
-from esphome.core import CORE, coroutine_with_priority
+    CONF_REBOOT_TIMEOUT, CONF_SERVICE, CONF_VARIABLES, CONF_SERVICES, CONF_TRIGGER_ID, CONF_EVENT
+from esphome.core import coroutine_with_priority
 
 DEPENDENCIES = ['network']
+AUTO_LOAD = ['async_tcp']
 
 api_ns = cg.esphome_ns.namespace('api')
 APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller)
 HomeAssistantServiceCallAction = api_ns.class_('HomeAssistantServiceCallAction', automation.Action)
-KeyValuePair = api_ns.class_('KeyValuePair')
-TemplatableKeyValuePair = api_ns.class_('TemplatableKeyValuePair')
 APIConnectedCondition = api_ns.class_('APIConnectedCondition', Condition)
 
-UserService = api_ns.class_('UserService', automation.Trigger)
-ServiceTypeArgument = api_ns.class_('ServiceTypeArgument')
-ServiceArgType = api_ns.enum('ServiceArgType')
-SERVICE_ARG_TYPES = {
-    'bool': ServiceArgType.SERVICE_ARG_TYPE_BOOL,
-    'int': ServiceArgType.SERVICE_ARG_TYPE_INT,
-    'float': ServiceArgType.SERVICE_ARG_TYPE_FLOAT,
-    'string': ServiceArgType.SERVICE_ARG_TYPE_STRING,
-}
+UserServiceTrigger = api_ns.class_('UserServiceTrigger', automation.Trigger)
+ListEntitiesServicesArgument = api_ns.class_('ListEntitiesServicesArgument')
 SERVICE_ARG_NATIVE_TYPES = {
     'bool': bool,
     'int': cg.int32,
     'float': float,
     'string': cg.std_string,
+    'bool[]': cg.std_vector.template(bool),
+    'int[]': cg.std_vector.template(cg.int32),
+    'float[]': cg.std_vector.template(float),
+    'string[]': cg.std_vector.template(cg.std_string),
 }
 
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.declare_id(APIServer),
     cv.Optional(CONF_PORT, default=6053): cv.port,
     cv.Optional(CONF_PASSWORD, default=''): cv.string_strict,
-    cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds,
+    cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds,
     cv.Optional(CONF_SERVICES): automation.validate_automation({
-        cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserService),
+        cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
         cv.Required(CONF_SERVICE): cv.valid_name,
         cv.Optional(CONF_VARIABLES, default={}): cv.Schema({
-            cv.validate_id_name: cv.one_of(*SERVICE_ARG_TYPES, lower=True),
+            cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
         }),
     }),
 }).extend(cv.COMPONENT_SCHEMA)
@@ -58,37 +54,30 @@ def to_code(config):
     for conf in config.get(CONF_SERVICES, []):
         template_args = []
         func_args = []
-        service_type_args = []
+        service_arg_names = []
         for name, var_ in conf[CONF_VARIABLES].items():
             native = SERVICE_ARG_NATIVE_TYPES[var_]
             template_args.append(native)
             func_args.append((native, name))
-            service_type_args.append(ServiceTypeArgument(name, SERVICE_ARG_TYPES[var_]))
+            service_arg_names.append(name)
         templ = cg.TemplateArguments(*template_args)
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], templ,
-                                   conf[CONF_SERVICE], service_type_args)
+                                   conf[CONF_SERVICE], service_arg_names)
         cg.add(var.register_user_service(trigger))
         yield automation.build_automation(trigger, func_args, conf)
 
     cg.add_define('USE_API')
-    if CORE.is_esp32:
-        cg.add_library('AsyncTCP', '1.0.3')
-    elif CORE.is_esp8266:
-        cg.add_library('ESPAsyncTCP', '1.2.0')
+    cg.add_global(api_ns.using)
 
 
+KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string)})
+
 HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.use_id(APIServer),
-    cv.Required(CONF_SERVICE): cv.string,
-    cv.Optional(CONF_DATA): cv.Schema({
-        cv.string: cv.string,
-    }),
-    cv.Optional(CONF_DATA_TEMPLATE): cv.Schema({
-        cv.string: cv.string,
-    }),
-    cv.Optional(CONF_VARIABLES): cv.Schema({
-        cv.string: cv.returning_lambda,
-    }),
+    cv.Required(CONF_SERVICE): cv.templatable(cv.string),
+    cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
+    cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
+    cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA,
 })
 
 
@@ -96,20 +85,54 @@ HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({
                             HOMEASSISTANT_SERVICE_ACTION_SCHEMA)
 def homeassistant_service_to_code(config, action_id, template_arg, args):
     serv = yield cg.get_variable(config[CONF_ID])
-    var = cg.new_Pvariable(action_id, template_arg, serv)
-    cg.add(var.set_service(config[CONF_SERVICE]))
-    if CONF_DATA in config:
-        datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA].items()]
-        cg.add(var.set_data(datas))
-    if CONF_DATA_TEMPLATE in config:
-        datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA_TEMPLATE].items()]
-        cg.add(var.set_data_template(datas))
-    if CONF_VARIABLES in config:
-        datas = []
-        for key, value in config[CONF_VARIABLES].items():
-            value_ = yield cg.process_lambda(value, [])
-            datas.append(TemplatableKeyValuePair(key, value_))
-        cg.add(var.set_variables(datas))
+    var = cg.new_Pvariable(action_id, template_arg, serv, False)
+    templ = yield cg.templatable(config[CONF_SERVICE], args, None)
+    cg.add(var.set_service(templ))
+    for key, value in config[CONF_DATA].items():
+        templ = yield cg.templatable(value, args, None)
+        cg.add(var.add_data(key, templ))
+    for key, value in config[CONF_DATA_TEMPLATE].items():
+        templ = yield cg.templatable(value, args, None)
+        cg.add(var.add_data_template(key, templ))
+    for key, value in config[CONF_VARIABLES].items():
+        templ = yield cg.templatable(value, args, None)
+        cg.add(var.add_variable(key, templ))
+    yield var
+
+
+def validate_homeassistant_event(value):
+    value = cv.string(value)
+    if not value.startswith(u'esphome.'):
+        raise cv.Invalid("ESPHome can only generate Home Assistant events that begin with "
+                         "esphome. For example 'esphome.xyz'")
+    return value
+
+
+HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.use_id(APIServer),
+    cv.Required(CONF_EVENT): validate_homeassistant_event,
+    cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
+    cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
+    cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA,
+})
+
+
+@automation.register_action('homeassistant.event', HomeAssistantServiceCallAction,
+                            HOMEASSISTANT_EVENT_ACTION_SCHEMA)
+def homeassistant_event_to_code(config, action_id, template_arg, args):
+    serv = yield cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, serv, True)
+    templ = yield cg.templatable(config[CONF_EVENT], args, None)
+    cg.add(var.set_service(templ))
+    for key, value in config[CONF_DATA].items():
+        templ = yield cg.templatable(value, args, None)
+        cg.add(var.add_data(key, templ))
+    for key, value in config[CONF_DATA_TEMPLATE].items():
+        templ = yield cg.templatable(value, args, None)
+        cg.add(var.add_data_template(key, templ))
+    for key, value in config[CONF_VARIABLES].items():
+        templ = yield cg.templatable(value, args, None)
+        cg.add(var.add_variable(key, templ))
     yield var
 
 
diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index 4d6f2ac9a7..175bd3858f 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -1,5 +1,45 @@
 syntax = "proto3";
 
+import "api_options.proto";
+
+service APIConnection {
+  rpc hello (HelloRequest) returns (HelloResponse) {
+    option (needs_setup_connection) = false;
+    option (needs_authentication) = false;
+  }
+  rpc connect (ConnectRequest) returns (ConnectResponse) {
+    option (needs_setup_connection) = false;
+    option (needs_authentication) = false;
+  }
+  rpc disconnect (DisconnectRequest) returns (DisconnectResponse) {
+    option (needs_setup_connection) = false;
+    option (needs_authentication) = false;
+  }
+  rpc ping (PingRequest) returns (PingResponse) {
+    option (needs_setup_connection) = false;
+    option (needs_authentication) = false;
+  }
+  rpc device_info (DeviceInfoRequest) returns (DeviceInfoResponse) {
+    option (needs_authentication) = false;
+  }
+  rpc list_entities (ListEntitiesRequest) returns (void) {}
+  rpc subscribe_states (SubscribeStatesRequest) returns (void) {}
+  rpc subscribe_logs (SubscribeLogsRequest) returns (void) {}
+  rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {}
+  rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {}
+  rpc get_time (GetTimeRequest) returns (GetTimeResponse) {
+    option (needs_authentication) = false;
+  }
+  rpc execute_service (ExecuteServiceRequest) returns (void) {}
+
+  rpc cover_command (CoverCommandRequest) returns (void) {}
+  rpc fan_command (FanCommandRequest) returns (void) {}
+  rpc light_command (LightCommandRequest) returns (void) {}
+  rpc switch_command (SwitchCommandRequest) returns (void) {}
+  rpc camera_image (CameraImageRequest) returns (void) {}
+  rpc climate_command (ClimateCommandRequest) returns (void) {}
+}
+
 
 // ==================== BASE PACKETS ====================
 
@@ -21,8 +61,11 @@ syntax = "proto3";
 
 // Message sent at the beginning of each connection
 // Can only be sent by the client and only at the beginning of the connection
-// ID: 1
 message HelloRequest {
+  option (id) = 1;
+  option (source) = SOURCE_CLIENT;
+  option (no_delay) = true;
+
   // Description of client (like User Agent)
   // For example "Home Assistant"
   // Not strictly necessary to send but nice for debugging
@@ -32,8 +75,11 @@ message HelloRequest {
 
 // Confirmation of successful connection request.
 // Can only be sent by the server and only at the beginning of the connection
-// ID: 2
 message HelloResponse {
+  option (id) = 2;
+  option (source) = SOURCE_SERVER;
+  option (no_delay) = true;
+
   // The version of the API to use. The _client_ (for example Home Assistant) needs to check
   // for compatibility and if necessary adopt to an older API.
   // Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_
@@ -49,49 +95,66 @@ message HelloResponse {
 
 // Message sent at the beginning of each connection to authenticate the client
 // Can only be sent by the client and only at the beginning of the connection
-// ID: 3
 message ConnectRequest {
+  option (id) = 3;
+  option (source) = SOURCE_CLIENT;
+  option (no_delay) = true;
+
   // The password to log in with
   string password = 1;
 }
 
 // Confirmation of successful connection. After this the connection is available for all traffic.
 // Can only be sent by the server and only at the beginning of the connection
-// ID: 4
 message ConnectResponse {
+  option (id) = 4;
+  option (source) = SOURCE_SERVER;
+  option (no_delay) = true;
+
   bool invalid_password = 1;
 }
 
 // Request to close the connection.
 // Can be sent by both the client and server
-// ID: 5
 message DisconnectRequest {
+  option (id) = 5;
+  option (source) = SOURCE_BOTH;
+  option (no_delay) = true;
+
   // Do not close the connection before the acknowledgement arrives
 }
 
-// ID: 6
 message DisconnectResponse {
+  option (id) = 6;
+  option (source) = SOURCE_BOTH;
+  option (no_delay) = true;
+
   // Empty - Both parties are required to close the connection after this
   // message has been received.
 }
 
-// ID: 7
 message PingRequest {
+  option (id) = 7;
+  option (source) = SOURCE_BOTH;
   // Empty
 }
 
-// ID: 8
 message PingResponse {
+  option (id) = 8;
+  option (source) = SOURCE_BOTH;
   // Empty
 }
 
-// ID: 9
 message DeviceInfoRequest {
+  option (id) = 9;
+  option (source) = SOURCE_CLIENT;
   // Empty
 }
 
-// ID: 10
 message DeviceInfoResponse {
+  option (id) = 10;
+  option (source) = SOURCE_SERVER;
+
   bool uses_password = 1;
 
   // The name of the node, given by "App.set_name()"
@@ -101,7 +164,7 @@ message DeviceInfoResponse {
   string mac_address = 3;
 
   // A string describing the ESPHome version. For example "1.10.0"
-  string esphome_core_version = 4;
+  string esphome_version = 4;
 
   // A string describing the date of compilation, this is generated by the compiler
   // and therefore may not be in the same format all the time.
@@ -114,22 +177,29 @@ message DeviceInfoResponse {
   bool has_deep_sleep = 7;
 }
 
-// ID: 11
 message ListEntitiesRequest {
+  option (id) = 11;
+  option (source) = SOURCE_CLIENT;
   // Empty
 }
-// ID: 19
 message ListEntitiesDoneResponse {
+  option (id) = 19;
+  option (source) = SOURCE_SERVER;
+  option (no_delay) = true;
   // Empty
 }
-// ID: 20
 message SubscribeStatesRequest {
+  option (id) = 20;
+  option (source) = SOURCE_CLIENT;
   // Empty
 }
 
 // ==================== BINARY SENSOR ====================
-// ID: 12
 message ListEntitiesBinarySensorResponse {
+  option (id) = 12;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_BINARY_SENSOR";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -138,15 +208,22 @@ message ListEntitiesBinarySensorResponse {
   string device_class = 5;
   bool is_status_binary_sensor = 6;
 }
-// ID: 21
 message BinarySensorStateResponse {
+  option (id) = 21;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_BINARY_SENSOR";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool state = 2;
 }
 
 // ==================== COVER ====================
-// ID: 13
 message ListEntitiesCoverResponse {
+  option (id) = 13;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_COVER";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -157,38 +234,47 @@ message ListEntitiesCoverResponse {
   bool supports_tilt = 7;
   string device_class = 8;
 }
-// ID: 22
-message CoverStateResponse {
-  fixed32 key = 1;
 
+enum LegacyCoverState {
+  LEGACY_COVER_STATE_OPEN = 0;
+  LEGACY_COVER_STATE_CLOSED = 1;
+}
+enum CoverOperation {
+  COVER_OPERATION_IDLE = 0;
+  COVER_OPERATION_IS_OPENING = 1;
+  COVER_OPERATION_IS_CLOSING = 2;
+}
+message CoverStateResponse {
+  option (id) = 22;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_COVER";
+  option (no_delay) = true;
+
+  fixed32 key = 1;
   // legacy: state has been removed in 1.13
   // clients/servers must still send/accept it until the next protocol change
-  enum LegacyCoverState {
-    OPEN = 0;
-    CLOSED = 1;
-  }
   LegacyCoverState legacy_state = 2;
 
   float position = 3;
   float tilt = 4;
-  enum CoverOperation {
-    IDLE = 0;
-    IS_OPENING = 1;
-    IS_CLOSING = 2;
-  }
   CoverOperation current_operation = 5;
 }
-// ID: 30
+
+enum LegacyCoverCommand {
+  LEGACY_COVER_COMMAND_OPEN = 0;
+  LEGACY_COVER_COMMAND_CLOSE = 1;
+  LEGACY_COVER_COMMAND_STOP = 2;
+}
 message CoverCommandRequest {
+  option (id) = 30;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_COVER";
+  option (no_delay) = true;
+
   fixed32 key = 1;
 
   // legacy: command has been removed in 1.13
   // clients/servers must still send/accept it until the next protocol change
-  enum LegacyCoverCommand {
-    OPEN = 0;
-    CLOSE = 1;
-    STOP = 2;
-  }
   bool has_legacy_command = 2;
   LegacyCoverCommand legacy_command = 3;
 
@@ -200,8 +286,11 @@ message CoverCommandRequest {
 }
 
 // ==================== FAN ====================
-// ID: 14
 message ListEntitiesFanResponse {
+  option (id) = 14;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_FAN";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -211,19 +300,27 @@ message ListEntitiesFanResponse {
   bool supports_speed = 6;
 }
 enum FanSpeed {
-  LOW = 0;
-  MEDIUM = 1;
-  HIGH = 2;
+  FAN_SPEED_LOW = 0;
+  FAN_SPEED_MEDIUM = 1;
+  FAN_SPEED_HIGH = 2;
 }
-// ID: 23
 message FanStateResponse {
+  option (id) = 23;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_FAN";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool state = 2;
   bool oscillating = 3;
   FanSpeed speed = 4;
 }
-// ID: 31
 message FanCommandRequest {
+  option (id) = 31;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_FAN";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool has_state = 2;
   bool state = 3;
@@ -234,8 +331,11 @@ message FanCommandRequest {
 }
 
 // ==================== LIGHT ====================
-// ID: 15
 message ListEntitiesLightResponse {
+  option (id) = 15;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_LIGHT";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -249,8 +349,12 @@ message ListEntitiesLightResponse {
   float max_mireds = 10;
   repeated string effects = 11;
 }
-// ID: 24
 message LightStateResponse {
+  option (id) = 24;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_LIGHT";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool state = 2;
   float brightness = 3;
@@ -261,8 +365,12 @@ message LightStateResponse {
   float color_temperature = 8;
   string effect = 9;
 }
-// ID: 32
 message LightCommandRequest {
+  option (id) = 32;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_LIGHT";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool has_state = 2;
   bool state = 3;
@@ -285,8 +393,11 @@ message LightCommandRequest {
 }
 
 // ==================== SENSOR ====================
-// ID: 16
 message ListEntitiesSensorResponse {
+  option (id) = 16;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_SENSOR";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -295,16 +406,24 @@ message ListEntitiesSensorResponse {
   string icon = 5;
   string unit_of_measurement = 6;
   int32 accuracy_decimals = 7;
+  bool force_update = 8;
 }
-// ID: 25
 message SensorStateResponse {
+  option (id) = 25;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_SENSOR";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   float state = 2;
 }
 
 // ==================== SWITCH ====================
-// ID: 17
 message ListEntitiesSwitchResponse {
+  option (id) = 17;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_SWITCH";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -313,20 +432,31 @@ message ListEntitiesSwitchResponse {
   string icon = 5;
   bool assumed_state = 6;
 }
-// ID: 26
 message SwitchStateResponse {
+  option (id) = 26;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_SWITCH";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool state = 2;
 }
-// ID: 33
 message SwitchCommandRequest {
+  option (id) = 33;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_SWITCH";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool state = 2;
 }
 
 // ==================== TEXT SENSOR ====================
-// ID: 18
 message ListEntitiesTextSensorResponse {
+  option (id) = 18;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_TEXT_SENSOR";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -334,29 +464,38 @@ message ListEntitiesTextSensorResponse {
 
   string icon = 5;
 }
-// ID: 27
 message TextSensorStateResponse {
+  option (id) = 27;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_TEXT_SENSOR";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   string state = 2;
 }
 
 // ==================== SUBSCRIBE LOGS ====================
 enum LogLevel {
-  NONE = 0;
-  ERROR = 1;
-  WARN = 2;
-  INFO = 3;
-  DEBUG = 4;
-  VERBOSE = 5;
-  VERY_VERBOSE = 6;
+  LOG_LEVEL_NONE = 0;
+  LOG_LEVEL_ERROR = 1;
+  LOG_LEVEL_WARN = 2;
+  LOG_LEVEL_INFO = 3;
+  LOG_LEVEL_DEBUG = 4;
+  LOG_LEVEL_VERBOSE = 5;
+  LOG_LEVEL_VERY_VERBOSE = 6;
 }
-// ID: 28
 message SubscribeLogsRequest {
+  option (id) = 28;
+  option (source) = SOURCE_CLIENT;
   LogLevel level = 1;
   bool dump_config = 2;
 }
-// ID: 29
 message SubscribeLogsResponse {
+  option (id) = 29;
+  option (source) = SOURCE_SERVER;
+  option (log) = false;
+  option (no_delay) = false;
+
   LogLevel level = 1;
   string tag = 2;
   string message = 3;
@@ -364,109 +503,159 @@ message SubscribeLogsResponse {
 }
 
 // ==================== HOMEASSISTANT.SERVICE ====================
-// ID: 34
-message SubscribeServiceCallsRequest {
-
+message SubscribeHomeassistantServicesRequest {
+  option (id) = 34;
+  option (source) = SOURCE_CLIENT;
 }
 
-// ID: 35
-message ServiceCallResponse {
+message HomeassistantServiceMap {
+  string key = 1;
+  string value = 2;
+}
+
+message HomeassistantServiceResponse {
+  option (id) = 35;
+  option (source) = SOURCE_SERVER;
+  option (no_delay) = true;
+
   string service = 1;
-  map<string, string> data = 2;
-  map<string, string> data_template = 3;
-  map<string, string> variables = 4;
+  repeated HomeassistantServiceMap data = 2;
+  repeated HomeassistantServiceMap data_template = 3;
+  repeated HomeassistantServiceMap variables = 4;
+  bool is_event = 5;
 }
 
 // ==================== IMPORT HOME ASSISTANT STATES ====================
 // 1. Client sends SubscribeHomeAssistantStatesRequest
 // 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async)
 // 3. Client sends HomeAssistantStateResponse for state changes.
-// ID: 38
 message SubscribeHomeAssistantStatesRequest {
-
+  option (id) = 38;
+  option (source) = SOURCE_CLIENT;
 }
 
-// ID: 39
 message SubscribeHomeAssistantStateResponse {
+  option (id) = 39;
+  option (source) = SOURCE_SERVER;
   string entity_id = 1;
 }
 
-// ID: 40
 message HomeAssistantStateResponse {
+  option (id) = 40;
+  option (source) = SOURCE_CLIENT;
+  option (no_delay) = true;
+
   string entity_id = 1;
   string state = 2;
 }
 
 // ==================== IMPORT TIME ====================
-// ID: 36
 message GetTimeRequest {
-
+  option (id) = 36;
+  option (source) = SOURCE_BOTH;
 }
 
-// ID: 37
 message GetTimeResponse {
+  option (id) = 37;
+  option (source) = SOURCE_BOTH;
+  option (no_delay) = true;
+
   fixed32 epoch_seconds = 1;
 }
 
 // ==================== USER-DEFINES SERVICES ====================
+enum ServiceArgType {
+  SERVICE_ARG_TYPE_BOOL = 0;
+  SERVICE_ARG_TYPE_INT = 1;
+  SERVICE_ARG_TYPE_FLOAT = 2;
+  SERVICE_ARG_TYPE_STRING = 3;
+  SERVICE_ARG_TYPE_BOOL_ARRAY = 4;
+  SERVICE_ARG_TYPE_INT_ARRAY = 5;
+  SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
+  SERVICE_ARG_TYPE_STRING_ARRAY = 7;
+}
 message ListEntitiesServicesArgument {
   string name = 1;
-  enum Type {
-    BOOL = 0;
-    INT = 1;
-    FLOAT = 2;
-    STRING = 3;
-  }
-  Type type = 2;
+  ServiceArgType type = 2;
 }
-// ID: 41
 message ListEntitiesServicesResponse {
+  option (id) = 41;
+  option (source) = SOURCE_SERVER;
+
   string name = 1;
   fixed32 key = 2;
   repeated ListEntitiesServicesArgument args = 3;
 }
 message ExecuteServiceArgument {
   bool bool_ = 1;
-  int32 int_ = 2;
+  int32 legacy_int = 2;
   float float_ = 3;
   string string_ = 4;
+  // ESPHome 1.14 (api v1.3) make int a signed value
+  sint32 int_ = 5;
+  repeated bool bool_array = 6 [packed=false];
+  repeated sint32 int_array = 7 [packed=false];
+  repeated float float_array = 8 [packed=false];
+  repeated string string_array = 9;
 }
-// ID: 42
 message ExecuteServiceRequest {
+  option (id) = 42;
+  option (source) = SOURCE_CLIENT;
+  option (no_delay) = true;
+
   fixed32 key = 1;
   repeated ExecuteServiceArgument args = 2;
 }
 
 // ==================== CAMERA ====================
-// ID: 43
 message ListEntitiesCameraResponse {
+  option (id) = 43;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_ESP32_CAMERA";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
   string unique_id = 4;
 }
 
-// ID: 44
 message CameraImageResponse {
+  option (id) = 44;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_ESP32_CAMERA";
+
   fixed32 key = 1;
   bytes data = 2;
   bool done = 3;
 }
-// ID: 45
 message CameraImageRequest {
+  option (id) = 45;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_ESP32_CAMERA";
+  option (no_delay) = true;
+
   bool single = 1;
   bool stream = 2;
 }
 
 // ==================== CLIMATE ====================
 enum ClimateMode {
-  OFF = 0;
-  AUTO = 1;
-  COOL = 2;
-  HEAT = 3;
+  CLIMATE_MODE_OFF = 0;
+  CLIMATE_MODE_AUTO = 1;
+  CLIMATE_MODE_COOL = 2;
+  CLIMATE_MODE_HEAT = 3;
+}
+enum ClimateAction {
+  CLIMATE_ACTION_OFF = 0;
+  // values same as mode for readability
+  CLIMATE_ACTION_COOLING = 2;
+  CLIMATE_ACTION_HEATING = 3;
 }
-// ID: 46
 message ListEntitiesClimateResponse {
+  option (id) = 46;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_CLIMATE";
+
   string object_id = 1;
   fixed32 key = 2;
   string name = 3;
@@ -479,9 +668,14 @@ message ListEntitiesClimateResponse {
   float visual_max_temperature = 9;
   float visual_temperature_step = 10;
   bool supports_away = 11;
+  bool supports_action = 12;
 }
-// ID: 47
 message ClimateStateResponse {
+  option (id) = 47;
+  option (source) = SOURCE_SERVER;
+  option (ifdef) = "USE_CLIMATE";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   ClimateMode mode = 2;
   float current_temperature = 3;
@@ -489,9 +683,14 @@ message ClimateStateResponse {
   float target_temperature_low = 5;
   float target_temperature_high = 6;
   bool away = 7;
+  ClimateAction action = 8;
 }
-// ID: 48
 message ClimateCommandRequest {
+  option (id) = 48;
+  option (source) = SOURCE_CLIENT;
+  option (ifdef) = "USE_CLIMATE";
+  option (no_delay) = true;
+
   fixed32 key = 1;
   bool has_mode = 2;
   ClimateMode mode = 3;
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
new file mode 100644
index 0000000000..4a595a3f99
--- /dev/null
+++ b/esphome/components/api/api_connection.cpp
@@ -0,0 +1,673 @@
+#include "api_connection.h"
+#include "esphome/core/log.h"
+#include "esphome/core/util.h"
+#include "esphome/core/version.h"
+
+#ifdef USE_DEEP_SLEEP
+#include "esphome/components/deep_sleep/deep_sleep_component.h"
+#endif
+#ifdef USE_HOMEASSISTANT_TIME
+#include "esphome/components/homeassistant/time/homeassistant_time.h"
+#endif
+
+namespace esphome {
+namespace api {
+
+static const char *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);
+
+  this->send_buffer_.reserve(64);
+  this->recv_buffer_.reserve(32);
+  this->client_info_ = this->client_->remoteIP().toString().c_str();
+  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)
+    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;
+}
+
+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();
+    return;
+  }
+  if (this->client_->disconnected()) {
+    // failsafe for disconnect logic
+    this->on_disconnect_();
+    return;
+  }
+  this->parse_recv_buffer_();
+
+  this->list_entities_iterator_.advance();
+  this->initial_state_iterator_.advance();
+
+  const uint32_t keepalive = 60000;
+  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();
+    }
+  } else if (millis() - 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);
+      this->set_nodelay(false);
+      bool success = this->send_buffer(buffer, 44);
+
+      if (success) {
+        this->image_reader_.consume_data(to_send);
+      }
+      if (success && done) {
+        this->image_reader_.return_image();
+      }
+    }
+  }
+#endif
+}
+
+std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
+  return App.get_name() + component_type + nameable->get_object_id();
+}
+
+#ifdef USE_BINARY_SENSOR
+bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
+  if (!this->state_subscription_)
+    return false;
+
+  BinarySensorStateResponse resp;
+  resp.key = binary_sensor->get_object_id_hash();
+  resp.state = state;
+  return this->send_binary_sensor_state_response(resp);
+}
+bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) {
+  ListEntitiesBinarySensorResponse msg;
+  msg.object_id = binary_sensor->get_object_id();
+  msg.key = binary_sensor->get_object_id_hash();
+  msg.name = binary_sensor->get_name();
+  msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor);
+  msg.device_class = binary_sensor->get_device_class();
+  msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
+  return this->send_list_entities_binary_sensor_response(msg);
+}
+#endif
+
+#ifdef USE_COVER
+bool APIConnection::send_cover_state(cover::Cover *cover) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = cover->get_traits();
+  CoverStateResponse resp{};
+  resp.key = cover->get_object_id_hash();
+  resp.legacy_state = (cover->position == cover::COVER_OPEN) ? LEGACY_COVER_STATE_OPEN : LEGACY_COVER_STATE_CLOSED;
+  resp.position = cover->position;
+  if (traits.get_supports_tilt())
+    resp.tilt = cover->tilt;
+  resp.current_operation = static_cast<EnumCoverOperation>(cover->current_operation);
+  return this->send_cover_state_response(resp);
+}
+bool APIConnection::send_cover_info(cover::Cover *cover) {
+  auto traits = cover->get_traits();
+  ListEntitiesCoverResponse msg;
+  msg.key = cover->get_object_id_hash();
+  msg.object_id = cover->get_object_id();
+  msg.name = cover->get_name();
+  msg.unique_id = get_default_unique_id("cover", cover);
+  msg.assumed_state = traits.get_is_assumed_state();
+  msg.supports_position = traits.get_supports_position();
+  msg.supports_tilt = traits.get_supports_tilt();
+  msg.device_class = cover->get_device_class();
+  return this->send_list_entities_cover_response(msg);
+}
+void APIConnection::cover_command(const CoverCommandRequest &msg) {
+  cover::Cover *cover = App.get_cover_by_key(msg.key);
+  if (cover == nullptr)
+    return;
+
+  auto call = cover->make_call();
+  if (msg.has_legacy_command) {
+    switch (msg.legacy_command) {
+      case LEGACY_COVER_COMMAND_OPEN:
+        call.set_command_open();
+        break;
+      case LEGACY_COVER_COMMAND_CLOSE:
+        call.set_command_close();
+        break;
+      case LEGACY_COVER_COMMAND_STOP:
+        call.set_command_stop();
+        break;
+    }
+  }
+  if (msg.has_position)
+    call.set_position(msg.position);
+  if (msg.has_tilt)
+    call.set_tilt(msg.tilt);
+  if (msg.stop)
+    call.set_command_stop();
+  call.perform();
+}
+#endif
+
+#ifdef USE_FAN
+bool APIConnection::send_fan_state(fan::FanState *fan) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = fan->get_traits();
+  FanStateResponse resp{};
+  resp.key = fan->get_object_id_hash();
+  resp.state = fan->state;
+  if (traits.supports_oscillation())
+    resp.oscillating = fan->oscillating;
+  if (traits.supports_speed())
+    resp.speed = static_cast<EnumFanSpeed>(fan->speed);
+  return this->send_fan_state_response(resp);
+}
+bool APIConnection::send_fan_info(fan::FanState *fan) {
+  auto traits = fan->get_traits();
+  ListEntitiesFanResponse msg;
+  msg.key = fan->get_object_id_hash();
+  msg.object_id = fan->get_object_id();
+  msg.name = fan->get_name();
+  msg.unique_id = get_default_unique_id("fan", fan);
+  msg.supports_oscillation = traits.supports_oscillation();
+  msg.supports_speed = traits.supports_speed();
+  return this->send_list_entities_fan_response(msg);
+}
+void APIConnection::fan_command(const FanCommandRequest &msg) {
+  fan::FanState *fan = App.get_fan_by_key(msg.key);
+  if (fan == nullptr)
+    return;
+
+  auto call = fan->make_call();
+  if (msg.has_state)
+    call.set_state(msg.state);
+  if (msg.has_oscillating)
+    call.set_oscillating(msg.oscillating);
+  if (msg.has_speed)
+    call.set_speed(static_cast<fan::FanSpeed>(msg.speed));
+  call.perform();
+}
+#endif
+
+#ifdef USE_LIGHT
+bool APIConnection::send_light_state(light::LightState *light) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = light->get_traits();
+  auto values = light->remote_values;
+  LightStateResponse resp{};
+
+  resp.key = light->get_object_id_hash();
+  resp.state = values.is_on();
+  if (traits.get_supports_brightness())
+    resp.brightness = values.get_brightness();
+  if (traits.get_supports_rgb()) {
+    resp.red = values.get_red();
+    resp.green = values.get_green();
+    resp.blue = values.get_blue();
+  }
+  if (traits.get_supports_rgb_white_value())
+    resp.white = values.get_white();
+  if (traits.get_supports_color_temperature())
+    resp.color_temperature = values.get_color_temperature();
+  if (light->supports_effects())
+    resp.effect = light->get_effect_name();
+  return this->send_light_state_response(resp);
+}
+bool APIConnection::send_light_info(light::LightState *light) {
+  auto traits = light->get_traits();
+  ListEntitiesLightResponse msg;
+  msg.key = light->get_object_id_hash();
+  msg.object_id = light->get_object_id();
+  msg.name = light->get_name();
+  msg.unique_id = get_default_unique_id("light", light);
+  msg.supports_brightness = traits.get_supports_brightness();
+  msg.supports_rgb = traits.get_supports_rgb();
+  msg.supports_white_value = traits.get_supports_rgb_white_value();
+  msg.supports_color_temperature = traits.get_supports_color_temperature();
+  if (msg.supports_color_temperature) {
+    msg.min_mireds = traits.get_min_mireds();
+    msg.max_mireds = traits.get_max_mireds();
+  }
+  if (light->supports_effects()) {
+    msg.effects.emplace_back("None");
+    for (auto *effect : light->get_effects())
+      msg.effects.push_back(effect->get_name());
+  }
+  return this->send_list_entities_light_response(msg);
+}
+void APIConnection::light_command(const LightCommandRequest &msg) {
+  light::LightState *light = App.get_light_by_key(msg.key);
+  if (light == nullptr)
+    return;
+
+  auto call = light->make_call();
+  if (msg.has_state)
+    call.set_state(msg.state);
+  if (msg.has_brightness)
+    call.set_brightness(msg.brightness);
+  if (msg.has_rgb) {
+    call.set_red(msg.red);
+    call.set_green(msg.green);
+    call.set_blue(msg.blue);
+  }
+  if (msg.has_white)
+    call.set_white(msg.white);
+  if (msg.has_color_temperature)
+    call.set_color_temperature(msg.color_temperature);
+  if (msg.has_transition_length)
+    call.set_transition_length(msg.transition_length);
+  if (msg.has_flash_length)
+    call.set_flash_length(msg.flash_length);
+  if (msg.has_effect)
+    call.set_effect(msg.effect);
+  call.perform();
+}
+#endif
+
+#ifdef USE_SENSOR
+bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) {
+  if (!this->state_subscription_)
+    return false;
+
+  SensorStateResponse resp{};
+  resp.key = sensor->get_object_id_hash();
+  resp.state = state;
+  return this->send_sensor_state_response(resp);
+}
+bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
+  ListEntitiesSensorResponse msg;
+  msg.key = sensor->get_object_id_hash();
+  msg.object_id = sensor->get_object_id();
+  msg.name = sensor->get_name();
+  msg.unique_id = sensor->unique_id();
+  if (msg.unique_id.empty())
+    msg.unique_id = get_default_unique_id("sensor", sensor);
+  msg.icon = sensor->get_icon();
+  msg.unit_of_measurement = sensor->get_unit_of_measurement();
+  msg.accuracy_decimals = sensor->get_accuracy_decimals();
+  return this->send_list_entities_sensor_response(msg);
+}
+#endif
+
+#ifdef USE_SWITCH
+bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) {
+  if (!this->state_subscription_)
+    return false;
+
+  SwitchStateResponse resp{};
+  resp.key = a_switch->get_object_id_hash();
+  resp.state = state;
+  return this->send_switch_state_response(resp);
+}
+bool APIConnection::send_switch_info(switch_::Switch *a_switch) {
+  ListEntitiesSwitchResponse msg;
+  msg.key = a_switch->get_object_id_hash();
+  msg.object_id = a_switch->get_object_id();
+  msg.name = a_switch->get_name();
+  msg.unique_id = get_default_unique_id("switch", a_switch);
+  msg.icon = a_switch->get_icon();
+  msg.assumed_state = a_switch->assumed_state();
+  return this->send_list_entities_switch_response(msg);
+}
+void APIConnection::switch_command(const SwitchCommandRequest &msg) {
+  switch_::Switch *a_switch = App.get_switch_by_key(msg.key);
+  if (a_switch == nullptr)
+    return;
+
+  if (msg.state)
+    a_switch->turn_on();
+  else
+    a_switch->turn_off();
+}
+#endif
+
+#ifdef USE_TEXT_SENSOR
+bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state) {
+  if (!this->state_subscription_)
+    return false;
+
+  TextSensorStateResponse resp{};
+  resp.key = text_sensor->get_object_id_hash();
+  resp.state = std::move(state);
+  return this->send_text_sensor_state_response(resp);
+}
+bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) {
+  ListEntitiesTextSensorResponse msg;
+  msg.key = text_sensor->get_object_id_hash();
+  msg.object_id = text_sensor->get_object_id();
+  msg.name = text_sensor->get_name();
+  msg.unique_id = text_sensor->unique_id();
+  if (msg.unique_id.empty())
+    msg.unique_id = get_default_unique_id("text_sensor", text_sensor);
+  msg.icon = text_sensor->get_icon();
+  return this->send_list_entities_text_sensor_response(msg);
+}
+#endif
+
+#ifdef USE_CLIMATE
+bool APIConnection::send_climate_state(climate::Climate *climate) {
+  if (!this->state_subscription_)
+    return false;
+
+  auto traits = climate->get_traits();
+  ClimateStateResponse resp{};
+  resp.key = climate->get_object_id_hash();
+  resp.mode = static_cast<EnumClimateMode>(climate->mode);
+  resp.action = static_cast<EnumClimateAction>(climate->action);
+  if (traits.get_supports_current_temperature())
+    resp.current_temperature = climate->current_temperature;
+  if (traits.get_supports_two_point_target_temperature()) {
+    resp.target_temperature_low = climate->target_temperature_low;
+    resp.target_temperature_high = climate->target_temperature_high;
+  } else {
+    resp.target_temperature = climate->target_temperature;
+  }
+  if (traits.get_supports_away())
+    resp.away = climate->away;
+  return this->send_climate_state_response(resp);
+}
+bool APIConnection::send_climate_info(climate::Climate *climate) {
+  auto traits = climate->get_traits();
+  ListEntitiesClimateResponse msg;
+  msg.key = climate->get_object_id_hash();
+  msg.object_id = climate->get_object_id();
+  msg.name = climate->get_name();
+  msg.unique_id = get_default_unique_id("climate", climate);
+  msg.supports_current_temperature = traits.get_supports_current_temperature();
+  msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
+  for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
+                    climate::CLIMATE_MODE_HEAT}) {
+    if (traits.supports_mode(mode))
+      msg.supported_modes.push_back(static_cast<EnumClimateMode>(mode));
+  }
+  msg.visual_min_temperature = traits.get_visual_min_temperature();
+  msg.visual_max_temperature = traits.get_visual_max_temperature();
+  msg.visual_temperature_step = traits.get_visual_temperature_step();
+  msg.supports_away = traits.get_supports_away();
+  msg.supports_action = traits.get_supports_action();
+  return this->send_list_entities_climate_response(msg);
+}
+void APIConnection::climate_command(const ClimateCommandRequest &msg) {
+  climate::Climate *climate = App.get_climate_by_key(msg.key);
+  if (climate == nullptr)
+    return;
+
+  auto call = climate->make_call();
+  if (msg.has_mode)
+    call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
+  if (msg.has_target_temperature)
+    call.set_target_temperature(msg.target_temperature);
+  if (msg.has_target_temperature_low)
+    call.set_target_temperature_low(msg.target_temperature_low);
+  if (msg.has_target_temperature_high)
+    call.set_target_temperature_high(msg.target_temperature_high);
+  if (msg.has_away)
+    call.set_away(msg.away);
+  call.perform();
+}
+#endif
+
+#ifdef USE_ESP32_CAMERA
+void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
+  if (!this->state_subscription_)
+    return;
+  if (this->image_reader_.available())
+    return;
+  this->image_reader_.set_image(image);
+}
+bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
+  ListEntitiesCameraResponse msg;
+  msg.key = camera->get_object_id_hash();
+  msg.object_id = camera->get_object_id();
+  msg.name = camera->get_name();
+  msg.unique_id = get_default_unique_id("camera", camera);
+  return this->send_list_entities_camera_response(msg);
+}
+void APIConnection::camera_image(const CameraImageRequest &msg) {
+  if (esp32_camera::global_esp32_camera == nullptr)
+    return;
+
+  if (msg.single)
+    esp32_camera::global_esp32_camera->request_image();
+  if (msg.stream)
+    esp32_camera::global_esp32_camera->request_stream();
+}
+#endif
+
+#ifdef USE_HOMEASSISTANT_TIME
+void APIConnection::on_get_time_response(const GetTimeResponse &value) {
+  if (homeassistant::global_homeassistant_time != nullptr)
+    homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
+}
+#endif
+
+bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
+  if (this->log_subscription_ < level)
+    return false;
+
+  this->set_nodelay(false);
+
+  // Send raw so that we don't copy too much
+  auto buffer = this->create_buffer();
+  // LogLevel level = 1;
+  buffer.encode_uint32(1, static_cast<uint32_t>(level));
+  // string tag = 2;
+  // buffer.encode_string(2, tag, strlen(tag));
+  // string message = 3;
+  buffer.encode_string(3, line, strlen(line));
+  // SubscribeLogsResponse - 29
+  bool success = this->send_buffer(buffer, 29);
+  if (!success) {
+    buffer = this->create_buffer();
+    // bool send_failed = 4;
+    buffer.encode_bool(4, true);
+    return this->send_buffer(buffer, 29);
+  } else {
+    return true;
+  }
+}
+
+HelloResponse APIConnection::hello(const HelloRequest &msg) {
+  this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
+  this->client_info_ += ")";
+  ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
+
+  HelloResponse resp;
+  resp.api_version_major = 1;
+  resp.api_version_minor = 3;
+  resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
+  this->connection_state_ = ConnectionState::CONNECTED;
+  return resp;
+}
+ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
+  bool correct = this->parent_->check_password(msg.password);
+
+  ConnectResponse resp;
+  // bool invalid_password = 1;
+  resp.invalid_password = !correct;
+  if (correct) {
+    ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
+    this->connection_state_ = ConnectionState::AUTHENTICATED;
+
+#ifdef USE_HOMEASSISTANT_TIME
+    if (homeassistant::global_homeassistant_time != nullptr) {
+      this->send_time_request();
+    }
+#endif
+  }
+  return resp;
+}
+DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
+  DeviceInfoResponse resp{};
+  resp.uses_password = this->parent_->uses_password();
+  resp.name = App.get_name();
+  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
+#ifdef USE_DEEP_SLEEP
+  resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
+#endif
+  return resp;
+}
+void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
+  for (auto &it : this->parent_->get_state_subs())
+    if (it.entity_id == msg.entity_id)
+      it.callback(msg.state);
+}
+void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
+  bool found = false;
+  for (auto *service : this->parent_->get_user_services()) {
+    if (service->execute_service(msg)) {
+      found = true;
+    }
+  }
+  if (!found) {
+    ESP_LOGV(TAG, "Could not find matching service!");
+  }
+}
+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;
+    if (!this->send_subscribe_home_assistant_state_response(resp)) {
+      this->on_fatal_error();
+      return;
+    }
+  }
+}
+bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
+  if (this->remove_)
+    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;
+    }
+  }
+
+  this->client_->add(reinterpret_cast<char *>(header.data()), header.size());
+  this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size());
+  bool ret = this->client_->send();
+  return ret;
+}
+void APIConnection::on_unauthenticated_access() {
+  ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
+  this->on_fatal_error();
+}
+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();
+}
+void APIConnection::on_fatal_error() {
+  ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
+  this->client_->close();
+  this->remove_ = true;
+}
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
new file mode 100644
index 0000000000..f9c7ffa28d
--- /dev/null
+++ b/esphome/components/api/api_connection.h
@@ -0,0 +1,178 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/application.h"
+#include "api_pb2.h"
+#include "api_pb2_service.h"
+#include "api_server.h"
+
+namespace esphome {
+namespace api {
+
+class APIConnection : public APIServerConnection {
+ public:
+  APIConnection(AsyncClient *client, APIServer *parent);
+  virtual ~APIConnection();
+
+  void disconnect_client();
+  void loop();
+
+  bool send_list_info_done() {
+    ListEntitiesDoneResponse resp;
+    return this->send_list_entities_done_response(resp);
+  }
+#ifdef USE_BINARY_SENSOR
+  bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
+  bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
+#endif
+#ifdef USE_COVER
+  bool send_cover_state(cover::Cover *cover);
+  bool send_cover_info(cover::Cover *cover);
+  void cover_command(const CoverCommandRequest &msg) override;
+#endif
+#ifdef USE_FAN
+  bool send_fan_state(fan::FanState *fan);
+  bool send_fan_info(fan::FanState *fan);
+  void fan_command(const FanCommandRequest &msg) override;
+#endif
+#ifdef USE_LIGHT
+  bool send_light_state(light::LightState *light);
+  bool send_light_info(light::LightState *light);
+  void light_command(const LightCommandRequest &msg) override;
+#endif
+#ifdef USE_SENSOR
+  bool send_sensor_state(sensor::Sensor *sensor, float state);
+  bool send_sensor_info(sensor::Sensor *sensor);
+#endif
+#ifdef USE_SWITCH
+  bool send_switch_state(switch_::Switch *a_switch, bool state);
+  bool send_switch_info(switch_::Switch *a_switch);
+  void switch_command(const SwitchCommandRequest &msg) override;
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
+  bool send_text_sensor_info(text_sensor::TextSensor *text_sensor);
+#endif
+#ifdef USE_ESP32_CAMERA
+  void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
+  bool send_camera_info(esp32_camera::ESP32Camera *camera);
+  void camera_image(const CameraImageRequest &msg) override;
+#endif
+#ifdef USE_CLIMATE
+  bool send_climate_state(climate::Climate *climate);
+  bool send_climate_info(climate::Climate *climate);
+  void climate_command(const ClimateCommandRequest &msg) override;
+#endif
+  bool send_log_message(int level, const char *tag, const char *line);
+  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
+    if (!this->service_call_subscription_)
+      return;
+    this->send_homeassistant_service_response(call);
+  }
+#ifdef USE_HOMEASSISTANT_TIME
+  void send_time_request() {
+    GetTimeRequest req;
+    this->send_get_time_request(req);
+  }
+#endif
+
+  void on_disconnect_response(const DisconnectResponse &value) override {
+    // we initiated disconnect_client
+    this->next_close_ = true;
+  }
+  void on_ping_response(const PingResponse &value) override {
+    // we initiated ping
+    this->sent_ping_ = false;
+  }
+  void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
+#ifdef USE_HOMEASSISTANT_TIME
+  void on_get_time_response(const GetTimeResponse &value) override;
+#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;
+  }
+  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(); }
+  void subscribe_states(const SubscribeStatesRequest &msg) override {
+    this->state_subscription_ = true;
+    this->initial_state_iterator_.begin();
+  }
+  void subscribe_logs(const SubscribeLogsRequest &msg) override {
+    this->log_subscription_ = msg.level;
+    if (msg.dump_config)
+      App.schedule_dump_config();
+  }
+  void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
+    this->service_call_subscription_ = true;
+  }
+  void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
+  GetTimeResponse get_time(const GetTimeRequest &msg) override {
+    // TODO
+    return {};
+  }
+  void execute_service(const ExecuteServiceRequest &msg) override;
+  bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
+  bool is_connection_setup() override {
+    return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated();
+  }
+  void on_fatal_error() override;
+  void on_unauthenticated_access() override;
+  void on_no_setup_connection() override;
+  ProtoWriteBuffer create_buffer() override {
+    this->send_buffer_.clear();
+    return {&this->send_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_();
+  void set_nodelay(bool nodelay) override {
+    if (nodelay == this->current_nodelay_)
+      return;
+    this->client_->setNoDelay(nodelay);
+    this->current_nodelay_ = nodelay;
+  }
+
+  enum class ConnectionState {
+    WAITING_FOR_HELLO,
+    CONNECTED,
+    AUTHENTICATED,
+  } connection_state_{ConnectionState::WAITING_FOR_HELLO};
+
+  bool remove_{false};
+
+  std::vector<uint8_t> send_buffer_;
+  std::vector<uint8_t> recv_buffer_;
+
+  std::string client_info_;
+#ifdef USE_ESP32_CAMERA
+  esp32_camera::CameraImageReader image_reader_;
+#endif
+
+  bool state_subscription_{false};
+  int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
+  uint32_t last_traffic_;
+  bool sent_ping_{false};
+  bool service_call_subscription_{false};
+  bool current_nodelay_{false};
+  bool next_close_{false};
+  AsyncClient *client_;
+  APIServer *parent_;
+  InitialStateIterator initial_state_iterator_;
+  ListEntitiesIterator list_entities_iterator_;
+};
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_message.h b/esphome/components/api/api_message.h
deleted file mode 100644
index 26c24073df..0000000000
--- a/esphome/components/api/api_message.h
+++ /dev/null
@@ -1,79 +0,0 @@
-#pragma once
-
-#include "esphome/core/component.h"
-#include "util.h"
-
-namespace esphome {
-namespace api {
-
-enum class APIMessageType {
-  HELLO_REQUEST = 1,
-  HELLO_RESPONSE = 2,
-  CONNECT_REQUEST = 3,
-  CONNECT_RESPONSE = 4,
-  DISCONNECT_REQUEST = 5,
-  DISCONNECT_RESPONSE = 6,
-  PING_REQUEST = 7,
-  PING_RESPONSE = 8,
-  DEVICE_INFO_REQUEST = 9,
-  DEVICE_INFO_RESPONSE = 10,
-
-  LIST_ENTITIES_REQUEST = 11,
-  LIST_ENTITIES_BINARY_SENSOR_RESPONSE = 12,
-  LIST_ENTITIES_COVER_RESPONSE = 13,
-  LIST_ENTITIES_FAN_RESPONSE = 14,
-  LIST_ENTITIES_LIGHT_RESPONSE = 15,
-  LIST_ENTITIES_SENSOR_RESPONSE = 16,
-  LIST_ENTITIES_SWITCH_RESPONSE = 17,
-  LIST_ENTITIES_TEXT_SENSOR_RESPONSE = 18,
-  LIST_ENTITIES_SERVICE_RESPONSE = 41,
-  LIST_ENTITIES_CAMERA_RESPONSE = 43,
-  LIST_ENTITIES_CLIMATE_RESPONSE = 46,
-  LIST_ENTITIES_DONE_RESPONSE = 19,
-
-  SUBSCRIBE_STATES_REQUEST = 20,
-  BINARY_SENSOR_STATE_RESPONSE = 21,
-  COVER_STATE_RESPONSE = 22,
-  FAN_STATE_RESPONSE = 23,
-  LIGHT_STATE_RESPONSE = 24,
-  SENSOR_STATE_RESPONSE = 25,
-  SWITCH_STATE_RESPONSE = 26,
-  TEXT_SENSOR_STATE_RESPONSE = 27,
-  CAMERA_IMAGE_RESPONSE = 44,
-  CLIMATE_STATE_RESPONSE = 47,
-
-  SUBSCRIBE_LOGS_REQUEST = 28,
-  SUBSCRIBE_LOGS_RESPONSE = 29,
-
-  COVER_COMMAND_REQUEST = 30,
-  FAN_COMMAND_REQUEST = 31,
-  LIGHT_COMMAND_REQUEST = 32,
-  SWITCH_COMMAND_REQUEST = 33,
-  CAMERA_IMAGE_REQUEST = 45,
-  CLIMATE_COMMAND_REQUEST = 48,
-
-  SUBSCRIBE_SERVICE_CALLS_REQUEST = 34,
-  SERVICE_CALL_RESPONSE = 35,
-  GET_TIME_REQUEST = 36,
-  GET_TIME_RESPONSE = 37,
-
-  SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST = 38,
-  SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE = 39,
-  HOME_ASSISTANT_STATE_RESPONSE = 40,
-
-  EXECUTE_SERVICE_REQUEST = 42,
-};
-
-class APIMessage {
- public:
-  void decode(const uint8_t *buffer, size_t length);
-  virtual bool decode_varint(uint32_t field_id, uint32_t value);
-  virtual bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len);
-  virtual bool decode_32bit(uint32_t field_id, uint32_t value);
-  virtual APIMessageType message_type() const = 0;
-
-  virtual void encode(APIBuffer &buffer);
-};
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto
new file mode 100644
index 0000000000..feaf39ba15
--- /dev/null
+++ b/esphome/components/api/api_options.proto
@@ -0,0 +1,24 @@
+syntax = "proto2";
+import "google/protobuf/descriptor.proto";
+
+
+enum APISourceType {
+    SOURCE_BOTH = 0;
+    SOURCE_SERVER = 1;
+    SOURCE_CLIENT = 2;
+}
+
+message void {}
+
+extend google.protobuf.MethodOptions {
+    optional bool needs_setup_connection = 1038 [default=true];
+    optional bool needs_authentication = 1039 [default=true];
+}
+
+extend google.protobuf.MessageOptions {
+    optional uint32 id = 1036 [default=0];
+    optional APISourceType source = 1037 [default=SOURCE_BOTH];
+    optional string ifdef = 1038;
+    optional bool log = 1039 [default=true];
+    optional bool no_delay = 1040 [default=false];
+}
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
new file mode 100644
index 0000000000..3f635d1cdb
--- /dev/null
+++ b/esphome/components/api/api_pb2.cpp
@@ -0,0 +1,2758 @@
+#include "api_pb2.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+template<> const char *proto_enum_to_string<EnumLegacyCoverState>(EnumLegacyCoverState value) {
+  switch (value) {
+    case LEGACY_COVER_STATE_OPEN:
+      return "LEGACY_COVER_STATE_OPEN";
+    case LEGACY_COVER_STATE_CLOSED:
+      return "LEGACY_COVER_STATE_CLOSED";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumCoverOperation>(EnumCoverOperation value) {
+  switch (value) {
+    case COVER_OPERATION_IDLE:
+      return "COVER_OPERATION_IDLE";
+    case COVER_OPERATION_IS_OPENING:
+      return "COVER_OPERATION_IS_OPENING";
+    case COVER_OPERATION_IS_CLOSING:
+      return "COVER_OPERATION_IS_CLOSING";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumLegacyCoverCommand>(EnumLegacyCoverCommand value) {
+  switch (value) {
+    case LEGACY_COVER_COMMAND_OPEN:
+      return "LEGACY_COVER_COMMAND_OPEN";
+    case LEGACY_COVER_COMMAND_CLOSE:
+      return "LEGACY_COVER_COMMAND_CLOSE";
+    case LEGACY_COVER_COMMAND_STOP:
+      return "LEGACY_COVER_COMMAND_STOP";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumFanSpeed>(EnumFanSpeed value) {
+  switch (value) {
+    case FAN_SPEED_LOW:
+      return "FAN_SPEED_LOW";
+    case FAN_SPEED_MEDIUM:
+      return "FAN_SPEED_MEDIUM";
+    case FAN_SPEED_HIGH:
+      return "FAN_SPEED_HIGH";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumLogLevel>(EnumLogLevel value) {
+  switch (value) {
+    case LOG_LEVEL_NONE:
+      return "LOG_LEVEL_NONE";
+    case LOG_LEVEL_ERROR:
+      return "LOG_LEVEL_ERROR";
+    case LOG_LEVEL_WARN:
+      return "LOG_LEVEL_WARN";
+    case LOG_LEVEL_INFO:
+      return "LOG_LEVEL_INFO";
+    case LOG_LEVEL_DEBUG:
+      return "LOG_LEVEL_DEBUG";
+    case LOG_LEVEL_VERBOSE:
+      return "LOG_LEVEL_VERBOSE";
+    case LOG_LEVEL_VERY_VERBOSE:
+      return "LOG_LEVEL_VERY_VERBOSE";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumServiceArgType>(EnumServiceArgType value) {
+  switch (value) {
+    case SERVICE_ARG_TYPE_BOOL:
+      return "SERVICE_ARG_TYPE_BOOL";
+    case SERVICE_ARG_TYPE_INT:
+      return "SERVICE_ARG_TYPE_INT";
+    case SERVICE_ARG_TYPE_FLOAT:
+      return "SERVICE_ARG_TYPE_FLOAT";
+    case SERVICE_ARG_TYPE_STRING:
+      return "SERVICE_ARG_TYPE_STRING";
+    case SERVICE_ARG_TYPE_BOOL_ARRAY:
+      return "SERVICE_ARG_TYPE_BOOL_ARRAY";
+    case SERVICE_ARG_TYPE_INT_ARRAY:
+      return "SERVICE_ARG_TYPE_INT_ARRAY";
+    case SERVICE_ARG_TYPE_FLOAT_ARRAY:
+      return "SERVICE_ARG_TYPE_FLOAT_ARRAY";
+    case SERVICE_ARG_TYPE_STRING_ARRAY:
+      return "SERVICE_ARG_TYPE_STRING_ARRAY";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumClimateMode>(EnumClimateMode value) {
+  switch (value) {
+    case CLIMATE_MODE_OFF:
+      return "CLIMATE_MODE_OFF";
+    case CLIMATE_MODE_AUTO:
+      return "CLIMATE_MODE_AUTO";
+    case CLIMATE_MODE_COOL:
+      return "CLIMATE_MODE_COOL";
+    case CLIMATE_MODE_HEAT:
+      return "CLIMATE_MODE_HEAT";
+    default:
+      return "UNKNOWN";
+  }
+}
+template<> const char *proto_enum_to_string<EnumClimateAction>(EnumClimateAction value) {
+  switch (value) {
+    case CLIMATE_ACTION_OFF:
+      return "CLIMATE_ACTION_OFF";
+    case CLIMATE_ACTION_COOLING:
+      return "CLIMATE_ACTION_COOLING";
+    case CLIMATE_ACTION_HEATING:
+      return "CLIMATE_ACTION_HEATING";
+    default:
+      return "UNKNOWN";
+  }
+}
+bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->client_info = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); }
+void HelloRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HelloRequest {\n");
+  out.append("  client_info: ");
+  out.append("'").append(this->client_info).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->api_version_major = value.as_uint32();
+      return true;
+    }
+    case 2: {
+      this->api_version_minor = value.as_uint32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool HelloResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 3: {
+      this->server_info = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HelloResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_uint32(1, this->api_version_major);
+  buffer.encode_uint32(2, this->api_version_minor);
+  buffer.encode_string(3, this->server_info);
+}
+void HelloResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HelloResponse {\n");
+  out.append("  api_version_major: ");
+  sprintf(buffer, "%u", this->api_version_major);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  api_version_minor: ");
+  sprintf(buffer, "%u", this->api_version_minor);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  server_info: ");
+  out.append("'").append(this->server_info).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->password = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); }
+void ConnectRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ConnectRequest {\n");
+  out.append("  password: ");
+  out.append("'").append(this->password).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->invalid_password = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); }
+void ConnectResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ConnectResponse {\n");
+  out.append("  invalid_password: ");
+  out.append(YESNO(this->invalid_password));
+  out.append("\n");
+  out.append("}");
+}
+void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {}
+void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
+void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {}
+void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
+void PingRequest::encode(ProtoWriteBuffer buffer) const {}
+void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }
+void PingResponse::encode(ProtoWriteBuffer buffer) const {}
+void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); }
+void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {}
+void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); }
+bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->uses_password = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->has_deep_sleep = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->mac_address = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->esphome_version = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->compilation_time = value.as_string();
+      return true;
+    }
+    case 6: {
+      this->model = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bool(1, this->uses_password);
+  buffer.encode_string(2, this->name);
+  buffer.encode_string(3, this->mac_address);
+  buffer.encode_string(4, this->esphome_version);
+  buffer.encode_string(5, this->compilation_time);
+  buffer.encode_string(6, this->model);
+  buffer.encode_bool(7, this->has_deep_sleep);
+}
+void DeviceInfoResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("DeviceInfoResponse {\n");
+  out.append("  uses_password: ");
+  out.append(YESNO(this->uses_password));
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  mac_address: ");
+  out.append("'").append(this->mac_address).append("'");
+  out.append("\n");
+
+  out.append("  esphome_version: ");
+  out.append("'").append(this->esphome_version).append("'");
+  out.append("\n");
+
+  out.append("  compilation_time: ");
+  out.append("'").append(this->compilation_time).append("'");
+  out.append("\n");
+
+  out.append("  model: ");
+  out.append("'").append(this->model).append("'");
+  out.append("\n");
+
+  out.append("  has_deep_sleep: ");
+  out.append(YESNO(this->has_deep_sleep));
+  out.append("\n");
+  out.append("}");
+}
+void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {}
+void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); }
+void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {}
+void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); }
+void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {}
+void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); }
+bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 6: {
+      this->is_status_binary_sensor = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->device_class = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesBinarySensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->device_class);
+  buffer.encode_bool(6, this->is_status_binary_sensor);
+}
+void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesBinarySensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+
+  out.append("  is_status_binary_sensor: ");
+  out.append(YESNO(this->is_status_binary_sensor));
+  out.append("\n");
+  out.append("}");
+}
+bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool BinarySensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+}
+void BinarySensorStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("BinarySensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->assumed_state = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_position = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supports_tilt = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 8: {
+      this->device_class = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesCoverResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->assumed_state);
+  buffer.encode_bool(6, this->supports_position);
+  buffer.encode_bool(7, this->supports_tilt);
+  buffer.encode_string(8, this->device_class);
+}
+void ListEntitiesCoverResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesCoverResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+
+  out.append("  supports_position: ");
+  out.append(YESNO(this->supports_position));
+  out.append("\n");
+
+  out.append("  supports_tilt: ");
+  out.append(YESNO(this->supports_tilt));
+  out.append("\n");
+
+  out.append("  device_class: ");
+  out.append("'").append(this->device_class).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->legacy_state = value.as_enum<EnumLegacyCoverState>();
+      return true;
+    }
+    case 5: {
+      this->current_operation = value.as_enum<EnumCoverOperation>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CoverStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->position = value.as_float();
+      return true;
+    }
+    case 4: {
+      this->tilt = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_enum<EnumLegacyCoverState>(2, this->legacy_state);
+  buffer.encode_float(3, this->position);
+  buffer.encode_float(4, this->tilt);
+  buffer.encode_enum<EnumCoverOperation>(5, this->current_operation);
+}
+void CoverStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CoverStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  legacy_state: ");
+  out.append(proto_enum_to_string<EnumLegacyCoverState>(this->legacy_state));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  tilt: ");
+  sprintf(buffer, "%g", this->tilt);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  current_operation: ");
+  out.append(proto_enum_to_string<EnumCoverOperation>(this->current_operation));
+  out.append("\n");
+  out.append("}");
+}
+bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_legacy_command = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->legacy_command = value.as_enum<EnumLegacyCoverCommand>();
+      return true;
+    }
+    case 4: {
+      this->has_position = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->has_tilt = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->stop = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 5: {
+      this->position = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->tilt = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_legacy_command);
+  buffer.encode_enum<EnumLegacyCoverCommand>(3, this->legacy_command);
+  buffer.encode_bool(4, this->has_position);
+  buffer.encode_float(5, this->position);
+  buffer.encode_bool(6, this->has_tilt);
+  buffer.encode_float(7, this->tilt);
+  buffer.encode_bool(8, this->stop);
+}
+void CoverCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CoverCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_legacy_command: ");
+  out.append(YESNO(this->has_legacy_command));
+  out.append("\n");
+
+  out.append("  legacy_command: ");
+  out.append(proto_enum_to_string<EnumLegacyCoverCommand>(this->legacy_command));
+  out.append("\n");
+
+  out.append("  has_position: ");
+  out.append(YESNO(this->has_position));
+  out.append("\n");
+
+  out.append("  position: ");
+  sprintf(buffer, "%g", this->position);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_tilt: ");
+  out.append(YESNO(this->has_tilt));
+  out.append("\n");
+
+  out.append("  tilt: ");
+  sprintf(buffer, "%g", this->tilt);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  stop: ");
+  out.append(YESNO(this->stop));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->supports_oscillation = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_speed = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesFanResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->supports_oscillation);
+  buffer.encode_bool(6, this->supports_speed);
+}
+void ListEntitiesFanResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesFanResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_oscillation: ");
+  out.append(YESNO(this->supports_oscillation));
+  out.append("\n");
+
+  out.append("  supports_speed: ");
+  out.append(YESNO(this->supports_speed));
+  out.append("\n");
+  out.append("}");
+}
+bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->oscillating = value.as_bool();
+      return true;
+    }
+    case 4: {
+      this->speed = value.as_enum<EnumFanSpeed>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+  buffer.encode_bool(3, this->oscillating);
+  buffer.encode_enum<EnumFanSpeed>(4, this->speed);
+}
+void FanStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("FanStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  oscillating: ");
+  out.append(YESNO(this->oscillating));
+  out.append("\n");
+
+  out.append("  speed: ");
+  out.append(proto_enum_to_string<EnumFanSpeed>(this->speed));
+  out.append("\n");
+  out.append("}");
+}
+bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 4: {
+      this->has_speed = value.as_bool();
+      return true;
+    }
+    case 5: {
+      this->speed = value.as_enum<EnumFanSpeed>();
+      return true;
+    }
+    case 6: {
+      this->has_oscillating = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->oscillating = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_state);
+  buffer.encode_bool(3, this->state);
+  buffer.encode_bool(4, this->has_speed);
+  buffer.encode_enum<EnumFanSpeed>(5, this->speed);
+  buffer.encode_bool(6, this->has_oscillating);
+  buffer.encode_bool(7, this->oscillating);
+}
+void FanCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("FanCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_speed: ");
+  out.append(YESNO(this->has_speed));
+  out.append("\n");
+
+  out.append("  speed: ");
+  out.append(proto_enum_to_string<EnumFanSpeed>(this->speed));
+  out.append("\n");
+
+  out.append("  has_oscillating: ");
+  out.append(YESNO(this->has_oscillating));
+  out.append("\n");
+
+  out.append("  oscillating: ");
+  out.append(YESNO(this->oscillating));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->supports_brightness = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_rgb = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supports_white_value = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->supports_color_temperature = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 11: {
+      this->effects.push_back(value.as_string());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesLightResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 9: {
+      this->min_mireds = value.as_float();
+      return true;
+    }
+    case 10: {
+      this->max_mireds = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->supports_brightness);
+  buffer.encode_bool(6, this->supports_rgb);
+  buffer.encode_bool(7, this->supports_white_value);
+  buffer.encode_bool(8, this->supports_color_temperature);
+  buffer.encode_float(9, this->min_mireds);
+  buffer.encode_float(10, this->max_mireds);
+  for (auto &it : this->effects) {
+    buffer.encode_string(11, it, true);
+  }
+}
+void ListEntitiesLightResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesLightResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_brightness: ");
+  out.append(YESNO(this->supports_brightness));
+  out.append("\n");
+
+  out.append("  supports_rgb: ");
+  out.append(YESNO(this->supports_rgb));
+  out.append("\n");
+
+  out.append("  supports_white_value: ");
+  out.append(YESNO(this->supports_white_value));
+  out.append("\n");
+
+  out.append("  supports_color_temperature: ");
+  out.append(YESNO(this->supports_color_temperature));
+  out.append("\n");
+
+  out.append("  min_mireds: ");
+  sprintf(buffer, "%g", this->min_mireds);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  max_mireds: ");
+  sprintf(buffer, "%g", this->max_mireds);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->effects) {
+    out.append("  effects: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 9: {
+      this->effect = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->brightness = value.as_float();
+      return true;
+    }
+    case 4: {
+      this->red = value.as_float();
+      return true;
+    }
+    case 5: {
+      this->green = value.as_float();
+      return true;
+    }
+    case 6: {
+      this->blue = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->white = value.as_float();
+      return true;
+    }
+    case 8: {
+      this->color_temperature = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void LightStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+  buffer.encode_float(3, this->brightness);
+  buffer.encode_float(4, this->red);
+  buffer.encode_float(5, this->green);
+  buffer.encode_float(6, this->blue);
+  buffer.encode_float(7, this->white);
+  buffer.encode_float(8, this->color_temperature);
+  buffer.encode_string(9, this->effect);
+}
+void LightStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("LightStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  brightness: ");
+  sprintf(buffer, "%g", this->brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  red: ");
+  sprintf(buffer, "%g", this->red);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  green: ");
+  sprintf(buffer, "%g", this->green);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  blue: ");
+  sprintf(buffer, "%g", this->blue);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  white: ");
+  sprintf(buffer, "%g", this->white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  color_temperature: ");
+  sprintf(buffer, "%g", this->color_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  effect: ");
+  out.append("'").append(this->effect).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_state = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->state = value.as_bool();
+      return true;
+    }
+    case 4: {
+      this->has_brightness = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->has_rgb = value.as_bool();
+      return true;
+    }
+    case 10: {
+      this->has_white = value.as_bool();
+      return true;
+    }
+    case 12: {
+      this->has_color_temperature = value.as_bool();
+      return true;
+    }
+    case 14: {
+      this->has_transition_length = value.as_bool();
+      return true;
+    }
+    case 15: {
+      this->transition_length = value.as_uint32();
+      return true;
+    }
+    case 16: {
+      this->has_flash_length = value.as_bool();
+      return true;
+    }
+    case 17: {
+      this->flash_length = value.as_uint32();
+      return true;
+    }
+    case 18: {
+      this->has_effect = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 19: {
+      this->effect = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 5: {
+      this->brightness = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->red = value.as_float();
+      return true;
+    }
+    case 8: {
+      this->green = value.as_float();
+      return true;
+    }
+    case 9: {
+      this->blue = value.as_float();
+      return true;
+    }
+    case 11: {
+      this->white = value.as_float();
+      return true;
+    }
+    case 13: {
+      this->color_temperature = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void LightCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_state);
+  buffer.encode_bool(3, this->state);
+  buffer.encode_bool(4, this->has_brightness);
+  buffer.encode_float(5, this->brightness);
+  buffer.encode_bool(6, this->has_rgb);
+  buffer.encode_float(7, this->red);
+  buffer.encode_float(8, this->green);
+  buffer.encode_float(9, this->blue);
+  buffer.encode_bool(10, this->has_white);
+  buffer.encode_float(11, this->white);
+  buffer.encode_bool(12, this->has_color_temperature);
+  buffer.encode_float(13, this->color_temperature);
+  buffer.encode_bool(14, this->has_transition_length);
+  buffer.encode_uint32(15, this->transition_length);
+  buffer.encode_bool(16, this->has_flash_length);
+  buffer.encode_uint32(17, this->flash_length);
+  buffer.encode_bool(18, this->has_effect);
+  buffer.encode_string(19, this->effect);
+}
+void LightCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("LightCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_state: ");
+  out.append(YESNO(this->has_state));
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+
+  out.append("  has_brightness: ");
+  out.append(YESNO(this->has_brightness));
+  out.append("\n");
+
+  out.append("  brightness: ");
+  sprintf(buffer, "%g", this->brightness);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_rgb: ");
+  out.append(YESNO(this->has_rgb));
+  out.append("\n");
+
+  out.append("  red: ");
+  sprintf(buffer, "%g", this->red);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  green: ");
+  sprintf(buffer, "%g", this->green);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  blue: ");
+  sprintf(buffer, "%g", this->blue);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_white: ");
+  out.append(YESNO(this->has_white));
+  out.append("\n");
+
+  out.append("  white: ");
+  sprintf(buffer, "%g", this->white);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_color_temperature: ");
+  out.append(YESNO(this->has_color_temperature));
+  out.append("\n");
+
+  out.append("  color_temperature: ");
+  sprintf(buffer, "%g", this->color_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_transition_length: ");
+  out.append(YESNO(this->has_transition_length));
+  out.append("\n");
+
+  out.append("  transition_length: ");
+  sprintf(buffer, "%u", this->transition_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_flash_length: ");
+  out.append(YESNO(this->has_flash_length));
+  out.append("\n");
+
+  out.append("  flash_length: ");
+  sprintf(buffer, "%u", this->flash_length);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_effect: ");
+  out.append(YESNO(this->has_effect));
+  out.append("\n");
+
+  out.append("  effect: ");
+  out.append("'").append(this->effect).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 7: {
+      this->accuracy_decimals = value.as_int32();
+      return true;
+    }
+    case 8: {
+      this->force_update = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    case 6: {
+      this->unit_of_measurement = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+  buffer.encode_string(6, this->unit_of_measurement);
+  buffer.encode_int32(7, this->accuracy_decimals);
+  buffer.encode_bool(8, this->force_update);
+}
+void ListEntitiesSensorResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesSensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  unit_of_measurement: ");
+  out.append("'").append(this->unit_of_measurement).append("'");
+  out.append("\n");
+
+  out.append("  accuracy_decimals: ");
+  sprintf(buffer, "%d", this->accuracy_decimals);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  force_update: ");
+  out.append(YESNO(this->force_update));
+  out.append("\n");
+  out.append("}");
+}
+bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 2: {
+      this->state = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SensorStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_float(2, this->state);
+}
+void SensorStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  sprintf(buffer, "%g", this->state);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 6: {
+      this->assumed_state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesSwitchResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+  buffer.encode_bool(6, this->assumed_state);
+}
+void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesSwitchResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+
+  out.append("  assumed_state: ");
+  out.append(YESNO(this->assumed_state));
+  out.append("\n");
+  out.append("}");
+}
+bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SwitchStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+}
+void SwitchStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SwitchStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->state);
+}
+void SwitchCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SwitchCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append(YESNO(this->state));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    case 5: {
+      this->icon = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesTextSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_string(5, this->icon);
+}
+void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesTextSensorResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  icon: ");
+  out.append("'").append(this->icon).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool TextSensorStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->state = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool TextSensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_string(2, this->state);
+}
+void TextSensorStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("TextSensorStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->level = value.as_enum<EnumLogLevel>();
+      return true;
+    }
+    case 2: {
+      this->dump_config = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_enum<EnumLogLevel>(1, this->level);
+  buffer.encode_bool(2, this->dump_config);
+}
+void SubscribeLogsRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SubscribeLogsRequest {\n");
+  out.append("  level: ");
+  out.append(proto_enum_to_string<EnumLogLevel>(this->level));
+  out.append("\n");
+
+  out.append("  dump_config: ");
+  out.append(YESNO(this->dump_config));
+  out.append("\n");
+  out.append("}");
+}
+bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->level = value.as_enum<EnumLogLevel>();
+      return true;
+    }
+    case 4: {
+      this->send_failed = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->tag = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->message = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_enum<EnumLogLevel>(1, this->level);
+  buffer.encode_string(2, this->tag);
+  buffer.encode_string(3, this->message);
+  buffer.encode_bool(4, this->send_failed);
+}
+void SubscribeLogsResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SubscribeLogsResponse {\n");
+  out.append("  level: ");
+  out.append(proto_enum_to_string<EnumLogLevel>(this->level));
+  out.append("\n");
+
+  out.append("  tag: ");
+  out.append("'").append(this->tag).append("'");
+  out.append("\n");
+
+  out.append("  message: ");
+  out.append("'").append(this->message).append("'");
+  out.append("\n");
+
+  out.append("  send_failed: ");
+  out.append(YESNO(this->send_failed));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {}
+void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
+  out.append("SubscribeHomeassistantServicesRequest {}");
+}
+bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_string();
+      return true;
+    }
+    case 2: {
+      this->value = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->key);
+  buffer.encode_string(2, this->value);
+}
+void HomeassistantServiceMap::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HomeassistantServiceMap {\n");
+  out.append("  key: ");
+  out.append("'").append(this->key).append("'");
+  out.append("\n");
+
+  out.append("  value: ");
+  out.append("'").append(this->value).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->is_event = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool HomeassistantServiceResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->service = value.as_string();
+      return true;
+    }
+    case 2: {
+      this->data.push_back(value.as_message<HomeassistantServiceMap>());
+      return true;
+    }
+    case 3: {
+      this->data_template.push_back(value.as_message<HomeassistantServiceMap>());
+      return true;
+    }
+    case 4: {
+      this->variables.push_back(value.as_message<HomeassistantServiceMap>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->service);
+  for (auto &it : this->data) {
+    buffer.encode_message<HomeassistantServiceMap>(2, it, true);
+  }
+  for (auto &it : this->data_template) {
+    buffer.encode_message<HomeassistantServiceMap>(3, it, true);
+  }
+  for (auto &it : this->variables) {
+    buffer.encode_message<HomeassistantServiceMap>(4, it, true);
+  }
+  buffer.encode_bool(5, this->is_event);
+}
+void HomeassistantServiceResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HomeassistantServiceResponse {\n");
+  out.append("  service: ");
+  out.append("'").append(this->service).append("'");
+  out.append("\n");
+
+  for (const auto &it : this->data) {
+    out.append("  data: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->data_template) {
+    out.append("  data_template: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->variables) {
+    out.append("  variables: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+
+  out.append("  is_event: ");
+  out.append(YESNO(this->is_event));
+  out.append("\n");
+  out.append("}");
+}
+void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {}
+void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
+  out.append("SubscribeHomeAssistantStatesRequest {}");
+}
+bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->entity_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->entity_id);
+}
+void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("SubscribeHomeAssistantStateResponse {\n");
+  out.append("  entity_id: ");
+  out.append("'").append(this->entity_id).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->entity_id = value.as_string();
+      return true;
+    }
+    case 2: {
+      this->state = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->entity_id);
+  buffer.encode_string(2, this->state);
+}
+void HomeAssistantStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("HomeAssistantStateResponse {\n");
+  out.append("  entity_id: ");
+  out.append("'").append(this->entity_id).append("'");
+  out.append("\n");
+
+  out.append("  state: ");
+  out.append("'").append(this->state).append("'");
+  out.append("\n");
+  out.append("}");
+}
+void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {}
+void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
+bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->epoch_seconds = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); }
+void GetTimeResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("GetTimeResponse {\n");
+  out.append("  epoch_seconds: ");
+  sprintf(buffer, "%u", this->epoch_seconds);
+  out.append(buffer);
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->type = value.as_enum<EnumServiceArgType>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->name = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->name);
+  buffer.encode_enum<EnumServiceArgType>(2, this->type);
+}
+void ListEntitiesServicesArgument::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesServicesArgument {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  type: ");
+  out.append(proto_enum_to_string<EnumServiceArgType>(this->type));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->args.push_back(value.as_message<ListEntitiesServicesArgument>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesServicesResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->name);
+  buffer.encode_fixed32(2, this->key);
+  for (auto &it : this->args) {
+    buffer.encode_message<ListEntitiesServicesArgument>(3, it, true);
+  }
+}
+void ListEntitiesServicesResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesServicesResponse {\n");
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->args) {
+    out.append("  args: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->bool_ = value.as_bool();
+      return true;
+    }
+    case 2: {
+      this->legacy_int = value.as_int32();
+      return true;
+    }
+    case 5: {
+      this->int_ = value.as_sint32();
+      return true;
+    }
+    case 6: {
+      this->bool_array.push_back(value.as_bool());
+      return true;
+    }
+    case 7: {
+      this->int_array.push_back(value.as_sint32());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 4: {
+      this->string_ = value.as_string();
+      return true;
+    }
+    case 9: {
+      this->string_array.push_back(value.as_string());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 3: {
+      this->float_ = value.as_float();
+      return true;
+    }
+    case 8: {
+      this->float_array.push_back(value.as_float());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bool(1, this->bool_);
+  buffer.encode_int32(2, this->legacy_int);
+  buffer.encode_float(3, this->float_);
+  buffer.encode_string(4, this->string_);
+  buffer.encode_sint32(5, this->int_);
+  for (auto it : this->bool_array) {
+    buffer.encode_bool(6, it, true);
+  }
+  for (auto &it : this->int_array) {
+    buffer.encode_sint32(7, it, true);
+  }
+  for (auto &it : this->float_array) {
+    buffer.encode_float(8, it, true);
+  }
+  for (auto &it : this->string_array) {
+    buffer.encode_string(9, it, true);
+  }
+}
+void ExecuteServiceArgument::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ExecuteServiceArgument {\n");
+  out.append("  bool_: ");
+  out.append(YESNO(this->bool_));
+  out.append("\n");
+
+  out.append("  legacy_int: ");
+  sprintf(buffer, "%d", this->legacy_int);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  float_: ");
+  sprintf(buffer, "%g", this->float_);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  string_: ");
+  out.append("'").append(this->string_).append("'");
+  out.append("\n");
+
+  out.append("  int_: ");
+  sprintf(buffer, "%d", this->int_);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto it : this->bool_array) {
+    out.append("  bool_array: ");
+    out.append(YESNO(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->int_array) {
+    out.append("  int_array: ");
+    sprintf(buffer, "%d", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->float_array) {
+    out.append("  float_array: ");
+    sprintf(buffer, "%g", it);
+    out.append(buffer);
+    out.append("\n");
+  }
+
+  for (const auto &it : this->string_array) {
+    out.append("  string_array: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->args.push_back(value.as_message<ExecuteServiceArgument>());
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  for (auto &it : this->args) {
+    buffer.encode_message<ExecuteServiceArgument>(2, it, true);
+  }
+}
+void ExecuteServiceRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ExecuteServiceRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  for (const auto &it : this->args) {
+    out.append("  args: ");
+    it.dump_to(out);
+    out.append("\n");
+  }
+  out.append("}");
+}
+bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesCameraResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+}
+void ListEntitiesCameraResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesCameraResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+  out.append("}");
+}
+bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 3: {
+      this->done = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CameraImageResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 2: {
+      this->data = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool CameraImageResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CameraImageResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_string(2, this->data);
+  buffer.encode_bool(3, this->done);
+}
+void CameraImageResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CameraImageResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  data: ");
+  out.append("'").append(this->data).append("'");
+  out.append("\n");
+
+  out.append("  done: ");
+  out.append(YESNO(this->done));
+  out.append("\n");
+  out.append("}");
+}
+bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 1: {
+      this->single = value.as_bool();
+      return true;
+    }
+    case 2: {
+      this->stream = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void CameraImageRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_bool(1, this->single);
+  buffer.encode_bool(2, this->stream);
+}
+void CameraImageRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("CameraImageRequest {\n");
+  out.append("  single: ");
+  out.append(YESNO(this->single));
+  out.append("\n");
+
+  out.append("  stream: ");
+  out.append(YESNO(this->stream));
+  out.append("\n");
+  out.append("}");
+}
+bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 5: {
+      this->supports_current_temperature = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->supports_two_point_target_temperature = value.as_bool();
+      return true;
+    }
+    case 7: {
+      this->supported_modes.push_back(value.as_enum<EnumClimateMode>());
+      return true;
+    }
+    case 11: {
+      this->supports_away = value.as_bool();
+      return true;
+    }
+    case 12: {
+      this->supports_action = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 1: {
+      this->object_id = value.as_string();
+      return true;
+    }
+    case 3: {
+      this->name = value.as_string();
+      return true;
+    }
+    case 4: {
+      this->unique_id = value.as_string();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 2: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 8: {
+      this->visual_min_temperature = value.as_float();
+      return true;
+    }
+    case 9: {
+      this->visual_max_temperature = value.as_float();
+      return true;
+    }
+    case 10: {
+      this->visual_temperature_step = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_string(1, this->object_id);
+  buffer.encode_fixed32(2, this->key);
+  buffer.encode_string(3, this->name);
+  buffer.encode_string(4, this->unique_id);
+  buffer.encode_bool(5, this->supports_current_temperature);
+  buffer.encode_bool(6, this->supports_two_point_target_temperature);
+  for (auto &it : this->supported_modes) {
+    buffer.encode_enum<EnumClimateMode>(7, it, true);
+  }
+  buffer.encode_float(8, this->visual_min_temperature);
+  buffer.encode_float(9, this->visual_max_temperature);
+  buffer.encode_float(10, this->visual_temperature_step);
+  buffer.encode_bool(11, this->supports_away);
+  buffer.encode_bool(12, this->supports_action);
+}
+void ListEntitiesClimateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ListEntitiesClimateResponse {\n");
+  out.append("  object_id: ");
+  out.append("'").append(this->object_id).append("'");
+  out.append("\n");
+
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  name: ");
+  out.append("'").append(this->name).append("'");
+  out.append("\n");
+
+  out.append("  unique_id: ");
+  out.append("'").append(this->unique_id).append("'");
+  out.append("\n");
+
+  out.append("  supports_current_temperature: ");
+  out.append(YESNO(this->supports_current_temperature));
+  out.append("\n");
+
+  out.append("  supports_two_point_target_temperature: ");
+  out.append(YESNO(this->supports_two_point_target_temperature));
+  out.append("\n");
+
+  for (const auto &it : this->supported_modes) {
+    out.append("  supported_modes: ");
+    out.append(proto_enum_to_string<EnumClimateMode>(it));
+    out.append("\n");
+  }
+
+  out.append("  visual_min_temperature: ");
+  sprintf(buffer, "%g", this->visual_min_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_max_temperature: ");
+  sprintf(buffer, "%g", this->visual_max_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  visual_temperature_step: ");
+  sprintf(buffer, "%g", this->visual_temperature_step);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  supports_away: ");
+  out.append(YESNO(this->supports_away));
+  out.append("\n");
+
+  out.append("  supports_action: ");
+  out.append(YESNO(this->supports_action));
+  out.append("\n");
+  out.append("}");
+}
+bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->mode = value.as_enum<EnumClimateMode>();
+      return true;
+    }
+    case 7: {
+      this->away = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->action = value.as_enum<EnumClimateAction>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 3: {
+      this->current_temperature = value.as_float();
+      return true;
+    }
+    case 4: {
+      this->target_temperature = value.as_float();
+      return true;
+    }
+    case 5: {
+      this->target_temperature_low = value.as_float();
+      return true;
+    }
+    case 6: {
+      this->target_temperature_high = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_enum<EnumClimateMode>(2, this->mode);
+  buffer.encode_float(3, this->current_temperature);
+  buffer.encode_float(4, this->target_temperature);
+  buffer.encode_float(5, this->target_temperature_low);
+  buffer.encode_float(6, this->target_temperature_high);
+  buffer.encode_bool(7, this->away);
+  buffer.encode_enum<EnumClimateAction>(8, this->action);
+}
+void ClimateStateResponse::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ClimateStateResponse {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string<EnumClimateMode>(this->mode));
+  out.append("\n");
+
+  out.append("  current_temperature: ");
+  sprintf(buffer, "%g", this->current_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature: ");
+  sprintf(buffer, "%g", this->target_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature_low: ");
+  sprintf(buffer, "%g", this->target_temperature_low);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  target_temperature_high: ");
+  sprintf(buffer, "%g", this->target_temperature_high);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  away: ");
+  out.append(YESNO(this->away));
+  out.append("\n");
+
+  out.append("  action: ");
+  out.append(proto_enum_to_string<EnumClimateAction>(this->action));
+  out.append("\n");
+  out.append("}");
+}
+bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
+  switch (field_id) {
+    case 2: {
+      this->has_mode = value.as_bool();
+      return true;
+    }
+    case 3: {
+      this->mode = value.as_enum<EnumClimateMode>();
+      return true;
+    }
+    case 4: {
+      this->has_target_temperature = value.as_bool();
+      return true;
+    }
+    case 6: {
+      this->has_target_temperature_low = value.as_bool();
+      return true;
+    }
+    case 8: {
+      this->has_target_temperature_high = value.as_bool();
+      return true;
+    }
+    case 10: {
+      this->has_away = value.as_bool();
+      return true;
+    }
+    case 11: {
+      this->away = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
+  switch (field_id) {
+    case 1: {
+      this->key = value.as_fixed32();
+      return true;
+    }
+    case 5: {
+      this->target_temperature = value.as_float();
+      return true;
+    }
+    case 7: {
+      this->target_temperature_low = value.as_float();
+      return true;
+    }
+    case 9: {
+      this->target_temperature_high = value.as_float();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
+  buffer.encode_fixed32(1, this->key);
+  buffer.encode_bool(2, this->has_mode);
+  buffer.encode_enum<EnumClimateMode>(3, this->mode);
+  buffer.encode_bool(4, this->has_target_temperature);
+  buffer.encode_float(5, this->target_temperature);
+  buffer.encode_bool(6, this->has_target_temperature_low);
+  buffer.encode_float(7, this->target_temperature_low);
+  buffer.encode_bool(8, this->has_target_temperature_high);
+  buffer.encode_float(9, this->target_temperature_high);
+  buffer.encode_bool(10, this->has_away);
+  buffer.encode_bool(11, this->away);
+}
+void ClimateCommandRequest::dump_to(std::string &out) const {
+  char buffer[64];
+  out.append("ClimateCommandRequest {\n");
+  out.append("  key: ");
+  sprintf(buffer, "%u", this->key);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_mode: ");
+  out.append(YESNO(this->has_mode));
+  out.append("\n");
+
+  out.append("  mode: ");
+  out.append(proto_enum_to_string<EnumClimateMode>(this->mode));
+  out.append("\n");
+
+  out.append("  has_target_temperature: ");
+  out.append(YESNO(this->has_target_temperature));
+  out.append("\n");
+
+  out.append("  target_temperature: ");
+  sprintf(buffer, "%g", this->target_temperature);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_target_temperature_low: ");
+  out.append(YESNO(this->has_target_temperature_low));
+  out.append("\n");
+
+  out.append("  target_temperature_low: ");
+  sprintf(buffer, "%g", this->target_temperature_low);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_target_temperature_high: ");
+  out.append(YESNO(this->has_target_temperature_high));
+  out.append("\n");
+
+  out.append("  target_temperature_high: ");
+  sprintf(buffer, "%g", this->target_temperature_high);
+  out.append(buffer);
+  out.append("\n");
+
+  out.append("  has_away: ");
+  out.append(YESNO(this->has_away));
+  out.append("\n");
+
+  out.append("  away: ");
+  out.append(YESNO(this->away));
+  out.append("\n");
+  out.append("}");
+}
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
new file mode 100644
index 0000000000..2e685959bf
--- /dev/null
+++ b/esphome/components/api/api_pb2.h
@@ -0,0 +1,695 @@
+#pragma once
+
+#include "proto.h"
+
+namespace esphome {
+namespace api {
+
+enum EnumLegacyCoverState : uint32_t {
+  LEGACY_COVER_STATE_OPEN = 0,
+  LEGACY_COVER_STATE_CLOSED = 1,
+};
+enum EnumCoverOperation : uint32_t {
+  COVER_OPERATION_IDLE = 0,
+  COVER_OPERATION_IS_OPENING = 1,
+  COVER_OPERATION_IS_CLOSING = 2,
+};
+enum EnumLegacyCoverCommand : uint32_t {
+  LEGACY_COVER_COMMAND_OPEN = 0,
+  LEGACY_COVER_COMMAND_CLOSE = 1,
+  LEGACY_COVER_COMMAND_STOP = 2,
+};
+enum EnumFanSpeed : uint32_t {
+  FAN_SPEED_LOW = 0,
+  FAN_SPEED_MEDIUM = 1,
+  FAN_SPEED_HIGH = 2,
+};
+enum EnumLogLevel : uint32_t {
+  LOG_LEVEL_NONE = 0,
+  LOG_LEVEL_ERROR = 1,
+  LOG_LEVEL_WARN = 2,
+  LOG_LEVEL_INFO = 3,
+  LOG_LEVEL_DEBUG = 4,
+  LOG_LEVEL_VERBOSE = 5,
+  LOG_LEVEL_VERY_VERBOSE = 6,
+};
+enum EnumServiceArgType : uint32_t {
+  SERVICE_ARG_TYPE_BOOL = 0,
+  SERVICE_ARG_TYPE_INT = 1,
+  SERVICE_ARG_TYPE_FLOAT = 2,
+  SERVICE_ARG_TYPE_STRING = 3,
+  SERVICE_ARG_TYPE_BOOL_ARRAY = 4,
+  SERVICE_ARG_TYPE_INT_ARRAY = 5,
+  SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
+  SERVICE_ARG_TYPE_STRING_ARRAY = 7,
+};
+enum EnumClimateMode : uint32_t {
+  CLIMATE_MODE_OFF = 0,
+  CLIMATE_MODE_AUTO = 1,
+  CLIMATE_MODE_COOL = 2,
+  CLIMATE_MODE_HEAT = 3,
+};
+enum EnumClimateAction : uint32_t {
+  CLIMATE_ACTION_OFF = 0,
+  CLIMATE_ACTION_COOLING = 2,
+  CLIMATE_ACTION_HEATING = 3,
+};
+class HelloRequest : public ProtoMessage {
+ public:
+  std::string client_info{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class HelloResponse : public ProtoMessage {
+ public:
+  uint32_t api_version_major{0};  // NOLINT
+  uint32_t api_version_minor{0};  // NOLINT
+  std::string server_info{};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ConnectRequest : public ProtoMessage {
+ public:
+  std::string password{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class ConnectResponse : public ProtoMessage {
+ public:
+  bool invalid_password{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class DisconnectRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class DisconnectResponse : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class PingRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class PingResponse : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class DeviceInfoRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class DeviceInfoResponse : public ProtoMessage {
+ public:
+  bool uses_password{false};       // NOLINT
+  std::string name{};              // NOLINT
+  std::string mac_address{};       // NOLINT
+  std::string esphome_version{};   // NOLINT
+  std::string compilation_time{};  // NOLINT
+  std::string model{};             // NOLINT
+  bool has_deep_sleep{false};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class ListEntitiesDoneResponse : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class SubscribeStatesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class ListEntitiesBinarySensorResponse : public ProtoMessage {
+ public:
+  std::string object_id{};              // NOLINT
+  uint32_t key{0};                      // NOLINT
+  std::string name{};                   // NOLINT
+  std::string unique_id{};              // NOLINT
+  std::string device_class{};           // NOLINT
+  bool is_status_binary_sensor{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class BinarySensorStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};    // NOLINT
+  bool state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesCoverResponse : public ProtoMessage {
+ public:
+  std::string object_id{};        // NOLINT
+  uint32_t key{0};                // NOLINT
+  std::string name{};             // NOLINT
+  std::string unique_id{};        // NOLINT
+  bool assumed_state{false};      // NOLINT
+  bool supports_position{false};  // NOLINT
+  bool supports_tilt{false};      // NOLINT
+  std::string device_class{};     // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class CoverStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                         // NOLINT
+  EnumLegacyCoverState legacy_state{};     // NOLINT
+  float position{0.0f};                    // NOLINT
+  float tilt{0.0f};                        // NOLINT
+  EnumCoverOperation current_operation{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class CoverCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                          // NOLINT
+  bool has_legacy_command{false};           // NOLINT
+  EnumLegacyCoverCommand legacy_command{};  // NOLINT
+  bool has_position{false};                 // NOLINT
+  float position{0.0f};                     // NOLINT
+  bool has_tilt{false};                     // NOLINT
+  float tilt{0.0f};                         // NOLINT
+  bool stop{false};                         // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesFanResponse : public ProtoMessage {
+ public:
+  std::string object_id{};           // NOLINT
+  uint32_t key{0};                   // NOLINT
+  std::string name{};                // NOLINT
+  std::string unique_id{};           // NOLINT
+  bool supports_oscillation{false};  // NOLINT
+  bool supports_speed{false};        // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class FanStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};          // NOLINT
+  bool state{false};        // NOLINT
+  bool oscillating{false};  // NOLINT
+  EnumFanSpeed speed{};     // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class FanCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};              // NOLINT
+  bool has_state{false};        // NOLINT
+  bool state{false};            // NOLINT
+  bool has_speed{false};        // NOLINT
+  EnumFanSpeed speed{};         // NOLINT
+  bool has_oscillating{false};  // NOLINT
+  bool oscillating{false};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesLightResponse : public ProtoMessage {
+ public:
+  std::string object_id{};                 // NOLINT
+  uint32_t key{0};                         // NOLINT
+  std::string name{};                      // NOLINT
+  std::string unique_id{};                 // NOLINT
+  bool supports_brightness{false};         // NOLINT
+  bool supports_rgb{false};                // NOLINT
+  bool supports_white_value{false};        // NOLINT
+  bool supports_color_temperature{false};  // NOLINT
+  float min_mireds{0.0f};                  // NOLINT
+  float max_mireds{0.0f};                  // NOLINT
+  std::vector<std::string> effects{};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class LightStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                // NOLINT
+  bool state{false};              // NOLINT
+  float brightness{0.0f};         // NOLINT
+  float red{0.0f};                // NOLINT
+  float green{0.0f};              // NOLINT
+  float blue{0.0f};               // NOLINT
+  float white{0.0f};              // NOLINT
+  float color_temperature{0.0f};  // NOLINT
+  std::string effect{};           // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class LightCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                    // NOLINT
+  bool has_state{false};              // NOLINT
+  bool state{false};                  // NOLINT
+  bool has_brightness{false};         // NOLINT
+  float brightness{0.0f};             // NOLINT
+  bool has_rgb{false};                // NOLINT
+  float red{0.0f};                    // NOLINT
+  float green{0.0f};                  // NOLINT
+  float blue{0.0f};                   // NOLINT
+  bool has_white{false};              // NOLINT
+  float white{0.0f};                  // NOLINT
+  bool has_color_temperature{false};  // NOLINT
+  float color_temperature{0.0f};      // NOLINT
+  bool has_transition_length{false};  // NOLINT
+  uint32_t transition_length{0};      // NOLINT
+  bool has_flash_length{false};       // NOLINT
+  uint32_t flash_length{0};           // NOLINT
+  bool has_effect{false};             // NOLINT
+  std::string effect{};               // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesSensorResponse : public ProtoMessage {
+ public:
+  std::string object_id{};            // NOLINT
+  uint32_t key{0};                    // NOLINT
+  std::string name{};                 // NOLINT
+  std::string unique_id{};            // NOLINT
+  std::string icon{};                 // NOLINT
+  std::string unit_of_measurement{};  // NOLINT
+  int32_t accuracy_decimals{0};       // NOLINT
+  bool force_update{false};           // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SensorStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};    // NOLINT
+  float state{0.0f};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+};
+class ListEntitiesSwitchResponse : public ProtoMessage {
+ public:
+  std::string object_id{};    // NOLINT
+  uint32_t key{0};            // NOLINT
+  std::string name{};         // NOLINT
+  std::string unique_id{};    // NOLINT
+  std::string icon{};         // NOLINT
+  bool assumed_state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SwitchStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};    // NOLINT
+  bool state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SwitchCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};    // NOLINT
+  bool state{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesTextSensorResponse : public ProtoMessage {
+ public:
+  std::string object_id{};  // NOLINT
+  uint32_t key{0};          // NOLINT
+  std::string name{};       // NOLINT
+  std::string unique_id{};  // NOLINT
+  std::string icon{};       // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class TextSensorStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};      // NOLINT
+  std::string state{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class SubscribeLogsRequest : public ProtoMessage {
+ public:
+  EnumLogLevel level{};     // NOLINT
+  bool dump_config{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeLogsResponse : public ProtoMessage {
+ public:
+  EnumLogLevel level{};     // NOLINT
+  std::string tag{};        // NOLINT
+  std::string message{};    // NOLINT
+  bool send_failed{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeHomeassistantServicesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class HomeassistantServiceMap : public ProtoMessage {
+ public:
+  std::string key{};    // NOLINT
+  std::string value{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class HomeassistantServiceResponse : public ProtoMessage {
+ public:
+  std::string service{};                                 // NOLINT
+  std::vector<HomeassistantServiceMap> data{};           // NOLINT
+  std::vector<HomeassistantServiceMap> data_template{};  // NOLINT
+  std::vector<HomeassistantServiceMap> variables{};      // NOLINT
+  bool is_event{false};                                  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class SubscribeHomeAssistantStatesRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class SubscribeHomeAssistantStateResponse : public ProtoMessage {
+ public:
+  std::string entity_id{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class HomeAssistantStateResponse : public ProtoMessage {
+ public:
+  std::string entity_id{};  // NOLINT
+  std::string state{};      // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class GetTimeRequest : public ProtoMessage {
+ public:
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+};
+class GetTimeResponse : public ProtoMessage {
+ public:
+  uint32_t epoch_seconds{0};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+};
+class ListEntitiesServicesArgument : public ProtoMessage {
+ public:
+  std::string name{};         // NOLINT
+  EnumServiceArgType type{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesServicesResponse : public ProtoMessage {
+ public:
+  std::string name{};                                // NOLINT
+  uint32_t key{0};                                   // NOLINT
+  std::vector<ListEntitiesServicesArgument> args{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class ExecuteServiceArgument : public ProtoMessage {
+ public:
+  bool bool_{false};                        // NOLINT
+  int32_t legacy_int{0};                    // NOLINT
+  float float_{0.0f};                       // NOLINT
+  std::string string_{};                    // NOLINT
+  int32_t int_{0};                          // NOLINT
+  std::vector<bool> bool_array{};           // NOLINT
+  std::vector<int32_t> int_array{};         // NOLINT
+  std::vector<float> float_array{};         // NOLINT
+  std::vector<std::string> string_array{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ExecuteServiceRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                             // NOLINT
+  std::vector<ExecuteServiceArgument> args{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class ListEntitiesCameraResponse : public ProtoMessage {
+ public:
+  std::string object_id{};  // NOLINT
+  uint32_t key{0};          // NOLINT
+  std::string name{};       // NOLINT
+  std::string unique_id{};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+};
+class CameraImageResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};     // NOLINT
+  std::string data{};  // NOLINT
+  bool done{false};    // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class CameraImageRequest : public ProtoMessage {
+ public:
+  bool single{false};  // NOLINT
+  bool stream{false};  // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ListEntitiesClimateResponse : public ProtoMessage {
+ public:
+  std::string object_id{};                            // NOLINT
+  uint32_t key{0};                                    // NOLINT
+  std::string name{};                                 // NOLINT
+  std::string unique_id{};                            // NOLINT
+  bool supports_current_temperature{false};           // NOLINT
+  bool supports_two_point_target_temperature{false};  // NOLINT
+  std::vector<EnumClimateMode> supported_modes{};     // NOLINT
+  float visual_min_temperature{0.0f};                 // NOLINT
+  float visual_max_temperature{0.0f};                 // NOLINT
+  float visual_temperature_step{0.0f};                // NOLINT
+  bool supports_away{false};                          // NOLINT
+  bool supports_action{false};                        // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ClimateStateResponse : public ProtoMessage {
+ public:
+  uint32_t key{0};                      // NOLINT
+  EnumClimateMode mode{};               // NOLINT
+  float current_temperature{0.0f};      // NOLINT
+  float target_temperature{0.0f};       // NOLINT
+  float target_temperature_low{0.0f};   // NOLINT
+  float target_temperature_high{0.0f};  // NOLINT
+  bool away{false};                     // NOLINT
+  EnumClimateAction action{};           // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+class ClimateCommandRequest : public ProtoMessage {
+ public:
+  uint32_t key{0};                          // NOLINT
+  bool has_mode{false};                     // NOLINT
+  EnumClimateMode mode{};                   // NOLINT
+  bool has_target_temperature{false};       // NOLINT
+  float target_temperature{0.0f};           // NOLINT
+  bool has_target_temperature_low{false};   // NOLINT
+  float target_temperature_low{0.0f};       // NOLINT
+  bool has_target_temperature_high{false};  // NOLINT
+  float target_temperature_high{0.0f};      // NOLINT
+  bool has_away{false};                     // NOLINT
+  bool away{false};                         // NOLINT
+  void encode(ProtoWriteBuffer buffer) const override;
+  void dump_to(std::string &out) const override;
+
+ protected:
+  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
+};
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp
new file mode 100644
index 0000000000..13e123c10f
--- /dev/null
+++ b/esphome/components/api/api_pb2_service.cpp
@@ -0,0 +1,582 @@
+#include "api_pb2_service.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+static const char *TAG = "api.service";
+
+bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) {
+  ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<HelloResponse>(msg, 2);
+}
+bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) {
+  ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<ConnectResponse>(msg, 4);
+}
+bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) {
+  ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<DisconnectRequest>(msg, 5);
+}
+bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) {
+  ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<DisconnectResponse>(msg, 6);
+}
+bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) {
+  ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<PingRequest>(msg, 7);
+}
+bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) {
+  ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<PingResponse>(msg, 8);
+}
+bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) {
+  ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<DeviceInfoResponse>(msg, 10);
+}
+bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<ListEntitiesDoneResponse>(msg, 19);
+}
+#ifdef USE_BINARY_SENSOR
+bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesBinarySensorResponse>(msg, 12);
+}
+#endif
+#ifdef USE_BINARY_SENSOR
+bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<BinarySensorStateResponse>(msg, 21);
+}
+#endif
+#ifdef USE_COVER
+bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesCoverResponse>(msg, 13);
+}
+#endif
+#ifdef USE_COVER
+bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<CoverStateResponse>(msg, 22);
+}
+#endif
+#ifdef USE_COVER
+#endif
+#ifdef USE_FAN
+bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesFanResponse>(msg, 14);
+}
+#endif
+#ifdef USE_FAN
+bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<FanStateResponse>(msg, 23);
+}
+#endif
+#ifdef USE_FAN
+#endif
+#ifdef USE_LIGHT
+bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesLightResponse>(msg, 15);
+}
+#endif
+#ifdef USE_LIGHT
+bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<LightStateResponse>(msg, 24);
+}
+#endif
+#ifdef USE_LIGHT
+#endif
+#ifdef USE_SENSOR
+bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesSensorResponse>(msg, 16);
+}
+#endif
+#ifdef USE_SENSOR
+bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<SensorStateResponse>(msg, 25);
+}
+#endif
+#ifdef USE_SWITCH
+bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesSwitchResponse>(msg, 17);
+}
+#endif
+#ifdef USE_SWITCH
+bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<SwitchStateResponse>(msg, 26);
+}
+#endif
+#ifdef USE_SWITCH
+#endif
+#ifdef USE_TEXT_SENSOR
+bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesTextSensorResponse>(msg, 18);
+}
+#endif
+#ifdef USE_TEXT_SENSOR
+bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<TextSensorStateResponse>(msg, 27);
+}
+#endif
+bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) {
+  this->set_nodelay(false);
+  return this->send_message_<SubscribeLogsResponse>(msg, 29);
+}
+bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) {
+  ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<HomeassistantServiceResponse>(msg, 35);
+}
+bool APIServerConnectionBase::send_subscribe_home_assistant_state_response(
+    const SubscribeHomeAssistantStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<SubscribeHomeAssistantStateResponse>(msg, 39);
+}
+bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) {
+  ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<GetTimeRequest>(msg, 36);
+}
+bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) {
+  ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<GetTimeResponse>(msg, 37);
+}
+bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesServicesResponse>(msg, 41);
+}
+#ifdef USE_ESP32_CAMERA
+bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesCameraResponse>(msg, 43);
+}
+#endif
+#ifdef USE_ESP32_CAMERA
+bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) {
+  ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<CameraImageResponse>(msg, 44);
+}
+#endif
+#ifdef USE_ESP32_CAMERA
+#endif
+#ifdef USE_CLIMATE
+bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) {
+  ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str());
+  this->set_nodelay(false);
+  return this->send_message_<ListEntitiesClimateResponse>(msg, 46);
+}
+#endif
+#ifdef USE_CLIMATE
+bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) {
+  ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str());
+  this->set_nodelay(true);
+  return this->send_message_<ClimateStateResponse>(msg, 47);
+}
+#endif
+#ifdef USE_CLIMATE
+#endif
+bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
+  switch (msg_type) {
+    case 1: {
+      HelloRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str());
+      this->on_hello_request(msg);
+      break;
+    }
+    case 3: {
+      ConnectRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str());
+      this->on_connect_request(msg);
+      break;
+    }
+    case 5: {
+      DisconnectRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
+      this->on_disconnect_request(msg);
+      break;
+    }
+    case 6: {
+      DisconnectResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
+      this->on_disconnect_response(msg);
+      break;
+    }
+    case 7: {
+      PingRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
+      this->on_ping_request(msg);
+      break;
+    }
+    case 8: {
+      PingResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
+      this->on_ping_response(msg);
+      break;
+    }
+    case 9: {
+      DeviceInfoRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
+      this->on_device_info_request(msg);
+      break;
+    }
+    case 11: {
+      ListEntitiesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
+      this->on_list_entities_request(msg);
+      break;
+    }
+    case 20: {
+      SubscribeStatesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
+      this->on_subscribe_states_request(msg);
+      break;
+    }
+    case 28: {
+      SubscribeLogsRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str());
+      this->on_subscribe_logs_request(msg);
+      break;
+    }
+    case 30: {
+#ifdef USE_COVER
+      CoverCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str());
+      this->on_cover_command_request(msg);
+#endif
+      break;
+    }
+    case 31: {
+#ifdef USE_FAN
+      FanCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str());
+      this->on_fan_command_request(msg);
+#endif
+      break;
+    }
+    case 32: {
+#ifdef USE_LIGHT
+      LightCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str());
+      this->on_light_command_request(msg);
+#endif
+      break;
+    }
+    case 33: {
+#ifdef USE_SWITCH
+      SwitchCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str());
+      this->on_switch_command_request(msg);
+#endif
+      break;
+    }
+    case 34: {
+      SubscribeHomeassistantServicesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
+      this->on_subscribe_homeassistant_services_request(msg);
+      break;
+    }
+    case 36: {
+      GetTimeRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
+      this->on_get_time_request(msg);
+      break;
+    }
+    case 37: {
+      GetTimeResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str());
+      this->on_get_time_response(msg);
+      break;
+    }
+    case 38: {
+      SubscribeHomeAssistantStatesRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
+      this->on_subscribe_home_assistant_states_request(msg);
+      break;
+    }
+    case 40: {
+      HomeAssistantStateResponse msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str());
+      this->on_home_assistant_state_response(msg);
+      break;
+    }
+    case 42: {
+      ExecuteServiceRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str());
+      this->on_execute_service_request(msg);
+      break;
+    }
+    case 45: {
+#ifdef USE_ESP32_CAMERA
+      CameraImageRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str());
+      this->on_camera_image_request(msg);
+#endif
+      break;
+    }
+    case 48: {
+#ifdef USE_CLIMATE
+      ClimateCommandRequest msg;
+      msg.decode(msg_data, msg_size);
+      ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str());
+      this->on_climate_command_request(msg);
+#endif
+      break;
+    }
+    default:
+      return false;
+  }
+  return true;
+}
+
+void APIServerConnection::on_hello_request(const HelloRequest &msg) {
+  HelloResponse ret = this->hello(msg);
+  if (!this->send_hello_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
+  ConnectResponse ret = this->connect(msg);
+  if (!this->send_connect_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
+  DisconnectResponse ret = this->disconnect(msg);
+  if (!this->send_disconnect_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_ping_request(const PingRequest &msg) {
+  PingResponse ret = this->ping(msg);
+  if (!this->send_ping_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  DeviceInfoResponse ret = this->device_info(msg);
+  if (!this->send_device_info_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->list_entities(msg);
+}
+void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_states(msg);
+}
+void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_logs(msg);
+}
+void APIServerConnection::on_subscribe_homeassistant_services_request(
+    const SubscribeHomeassistantServicesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_homeassistant_services(msg);
+}
+void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->subscribe_home_assistant_states(msg);
+}
+void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  GetTimeResponse ret = this->get_time(msg);
+  if (!this->send_get_time_response(ret)) {
+    this->on_fatal_error();
+  }
+}
+void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->execute_service(msg);
+}
+#ifdef USE_COVER
+void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->cover_command(msg);
+}
+#endif
+#ifdef USE_FAN
+void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->fan_command(msg);
+}
+#endif
+#ifdef USE_LIGHT
+void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->light_command(msg);
+}
+#endif
+#ifdef USE_SWITCH
+void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->switch_command(msg);
+}
+#endif
+#ifdef USE_ESP32_CAMERA
+void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->camera_image(msg);
+}
+#endif
+#ifdef USE_CLIMATE
+void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
+  if (!this->is_connection_setup()) {
+    this->on_no_setup_connection();
+    return;
+  }
+  if (!this->is_authenticated()) {
+    this->on_unauthenticated_access();
+    return;
+  }
+  this->climate_command(msg);
+}
+#endif
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h
new file mode 100644
index 0000000000..16662811fe
--- /dev/null
+++ b/esphome/components/api/api_pb2_service.h
@@ -0,0 +1,183 @@
+#pragma once
+
+#include "api_pb2.h"
+#include "esphome/core/defines.h"
+
+namespace esphome {
+namespace api {
+
+class APIServerConnectionBase : public ProtoService {
+ public:
+  virtual void on_hello_request(const HelloRequest &value){};
+  bool send_hello_response(const HelloResponse &msg);
+  virtual void on_connect_request(const ConnectRequest &value){};
+  bool send_connect_response(const ConnectResponse &msg);
+  bool send_disconnect_request(const DisconnectRequest &msg);
+  virtual void on_disconnect_request(const DisconnectRequest &value){};
+  bool send_disconnect_response(const DisconnectResponse &msg);
+  virtual void on_disconnect_response(const DisconnectResponse &value){};
+  bool send_ping_request(const PingRequest &msg);
+  virtual void on_ping_request(const PingRequest &value){};
+  bool send_ping_response(const PingResponse &msg);
+  virtual void on_ping_response(const PingResponse &value){};
+  virtual void on_device_info_request(const DeviceInfoRequest &value){};
+  bool send_device_info_response(const DeviceInfoResponse &msg);
+  virtual void on_list_entities_request(const ListEntitiesRequest &value){};
+  bool send_list_entities_done_response(const ListEntitiesDoneResponse &msg);
+  virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){};
+#ifdef USE_BINARY_SENSOR
+  bool send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg);
+#endif
+#ifdef USE_BINARY_SENSOR
+  bool send_binary_sensor_state_response(const BinarySensorStateResponse &msg);
+#endif
+#ifdef USE_COVER
+  bool send_list_entities_cover_response(const ListEntitiesCoverResponse &msg);
+#endif
+#ifdef USE_COVER
+  bool send_cover_state_response(const CoverStateResponse &msg);
+#endif
+#ifdef USE_COVER
+  virtual void on_cover_command_request(const CoverCommandRequest &value){};
+#endif
+#ifdef USE_FAN
+  bool send_list_entities_fan_response(const ListEntitiesFanResponse &msg);
+#endif
+#ifdef USE_FAN
+  bool send_fan_state_response(const FanStateResponse &msg);
+#endif
+#ifdef USE_FAN
+  virtual void on_fan_command_request(const FanCommandRequest &value){};
+#endif
+#ifdef USE_LIGHT
+  bool send_list_entities_light_response(const ListEntitiesLightResponse &msg);
+#endif
+#ifdef USE_LIGHT
+  bool send_light_state_response(const LightStateResponse &msg);
+#endif
+#ifdef USE_LIGHT
+  virtual void on_light_command_request(const LightCommandRequest &value){};
+#endif
+#ifdef USE_SENSOR
+  bool send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg);
+#endif
+#ifdef USE_SENSOR
+  bool send_sensor_state_response(const SensorStateResponse &msg);
+#endif
+#ifdef USE_SWITCH
+  bool send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg);
+#endif
+#ifdef USE_SWITCH
+  bool send_switch_state_response(const SwitchStateResponse &msg);
+#endif
+#ifdef USE_SWITCH
+  virtual void on_switch_command_request(const SwitchCommandRequest &value){};
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg);
+#endif
+#ifdef USE_TEXT_SENSOR
+  bool send_text_sensor_state_response(const TextSensorStateResponse &msg);
+#endif
+  virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
+  bool send_subscribe_logs_response(const SubscribeLogsResponse &msg);
+  virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
+  bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg);
+  virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
+  bool send_subscribe_home_assistant_state_response(const SubscribeHomeAssistantStateResponse &msg);
+  virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
+  bool send_get_time_request(const GetTimeRequest &msg);
+  virtual void on_get_time_request(const GetTimeRequest &value){};
+  bool send_get_time_response(const GetTimeResponse &msg);
+  virtual void on_get_time_response(const GetTimeResponse &value){};
+  bool send_list_entities_services_response(const ListEntitiesServicesResponse &msg);
+  virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
+#ifdef USE_ESP32_CAMERA
+  bool send_list_entities_camera_response(const ListEntitiesCameraResponse &msg);
+#endif
+#ifdef USE_ESP32_CAMERA
+  bool send_camera_image_response(const CameraImageResponse &msg);
+#endif
+#ifdef USE_ESP32_CAMERA
+  virtual void on_camera_image_request(const CameraImageRequest &value){};
+#endif
+#ifdef USE_CLIMATE
+  bool send_list_entities_climate_response(const ListEntitiesClimateResponse &msg);
+#endif
+#ifdef USE_CLIMATE
+  bool send_climate_state_response(const ClimateStateResponse &msg);
+#endif
+#ifdef USE_CLIMATE
+  virtual void on_climate_command_request(const ClimateCommandRequest &value){};
+#endif
+ protected:
+  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
+};
+
+class APIServerConnection : public APIServerConnectionBase {
+ public:
+  virtual HelloResponse hello(const HelloRequest &msg) = 0;
+  virtual ConnectResponse connect(const ConnectRequest &msg) = 0;
+  virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0;
+  virtual PingResponse ping(const PingRequest &msg) = 0;
+  virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0;
+  virtual void list_entities(const ListEntitiesRequest &msg) = 0;
+  virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
+  virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
+  virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
+  virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
+  virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
+  virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
+#ifdef USE_COVER
+  virtual void cover_command(const CoverCommandRequest &msg) = 0;
+#endif
+#ifdef USE_FAN
+  virtual void fan_command(const FanCommandRequest &msg) = 0;
+#endif
+#ifdef USE_LIGHT
+  virtual void light_command(const LightCommandRequest &msg) = 0;
+#endif
+#ifdef USE_SWITCH
+  virtual void switch_command(const SwitchCommandRequest &msg) = 0;
+#endif
+#ifdef USE_ESP32_CAMERA
+  virtual void camera_image(const CameraImageRequest &msg) = 0;
+#endif
+#ifdef USE_CLIMATE
+  virtual void climate_command(const ClimateCommandRequest &msg) = 0;
+#endif
+ protected:
+  void on_hello_request(const HelloRequest &msg) override;
+  void on_connect_request(const ConnectRequest &msg) override;
+  void on_disconnect_request(const DisconnectRequest &msg) override;
+  void on_ping_request(const PingRequest &msg) override;
+  void on_device_info_request(const DeviceInfoRequest &msg) override;
+  void on_list_entities_request(const ListEntitiesRequest &msg) override;
+  void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
+  void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
+  void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
+  void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
+  void on_get_time_request(const GetTimeRequest &msg) override;
+  void on_execute_service_request(const ExecuteServiceRequest &msg) override;
+#ifdef USE_COVER
+  void on_cover_command_request(const CoverCommandRequest &msg) override;
+#endif
+#ifdef USE_FAN
+  void on_fan_command_request(const FanCommandRequest &msg) override;
+#endif
+#ifdef USE_LIGHT
+  void on_light_command_request(const LightCommandRequest &msg) override;
+#endif
+#ifdef USE_SWITCH
+  void on_switch_command_request(const SwitchCommandRequest &msg) override;
+#endif
+#ifdef USE_ESP32_CAMERA
+  void on_camera_image_request(const CameraImageRequest &msg) override;
+#endif
+#ifdef USE_CLIMATE
+  void on_climate_command_request(const ClimateCommandRequest &msg) override;
+#endif
+};
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index cde6f1e875..25ae9a98a3 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -1,19 +1,11 @@
-#include <utility>
-
 #include "api_server.h"
-#include "basic_messages.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/version.h"
 
-#ifdef USE_DEEP_SLEEP
-#include "esphome/components/deep_sleep/deep_sleep_component.h"
-#endif
-#ifdef USE_HOMEASSISTANT_TIME
-#include "esphome/components/homeassistant/time/homeassistant_time.h"
-#endif
 #ifdef USE_LOGGER
 #include "esphome/components/logger/logger.h"
 #endif
@@ -210,9 +202,9 @@ void APIServer::set_port(uint16_t port) { this->port_ = port; }
 APIServer *global_api_server = nullptr;
 
 void APIServer::set_password(const std::string &password) { this->password_ = password; }
-void APIServer::send_service_call(ServiceCallResponse &call) {
+void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
   for (auto *client : this->clients_) {
-    client->send_service_call(call);
+    client->send_homeassistant_service_call(call);
   }
 }
 APIServer::APIServer() { global_api_server = this; }
@@ -238,965 +230,10 @@ void APIServer::request_time() {
 bool APIServer::is_connected() const { return !this->clients_.empty(); }
 void APIServer::on_shutdown() {
   for (auto *c : this->clients_) {
-    c->send_disconnect_request();
+    c->send_disconnect_request(DisconnectRequest());
   }
   delay(10);
 }
 
-// APIConnection
-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);
-
-  this->send_buffer_.reserve(64);
-  this->recv_buffer_.reserve(32);
-  this->client_info_ = this->client_->remoteIP().toString().c_str();
-  this->last_traffic_ = millis();
-}
-APIConnection::~APIConnection() { delete this->client_; }
-void APIConnection::on_error_(int8_t error) {
-  // disconnect will also be called, nothing to do here
-  this->remove_ = true;
-}
-void APIConnection::on_disconnect_() {
-  // delete self, generally unsafe but not in this case.
-  this->remove_ = true;
-}
-void APIConnection::on_timeout_(uint32_t time) { this->disconnect_client(); }
-void APIConnection::on_data_(uint8_t *buf, size_t len) {
-  if (len == 0 || buf == nullptr)
-    return;
-
-  this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
-  // TODO: On ESP32, use queue to notify main thread of new data
-}
-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->fatal_error_();
-      return;
-    }
-    uint32_t i = 1;
-    const uint32_t size = this->recv_buffer_.size();
-    uint32_t msg_size = 0;
-    while (i < size) {
-      const uint8_t dat = this->recv_buffer_[i];
-      msg_size |= (dat & 0x7F);
-      // consume
-      i += 1;
-      if ((dat & 0x80) == 0x00) {
-        break;
-      } else {
-        msg_size <<= 7;
-      }
-    }
-    if (i == size)
-      // not enough data there yet
-      return;
-
-    uint32_t msg_type = 0;
-    bool msg_type_done = false;
-    while (i < size) {
-      const uint8_t dat = this->recv_buffer_[i];
-      msg_type |= (dat & 0x7F);
-      // consume
-      i += 1;
-      if ((dat & 0x80) == 0x00) {
-        msg_type_done = true;
-        break;
-      } else {
-        msg_type <<= 7;
-      }
-    }
-    if (!msg_type_done)
-      // not enough data there yet
-      return;
-
-    if (size - i < msg_size)
-      // message body not fully received
-      return;
-
-    // ESP_LOGVV(TAG, "RECV Message: Size=%u Type=%u", msg_size, msg_type);
-
-    if (!this->valid_rx_message_type_(msg_type)) {
-      ESP_LOGE(TAG, "Not a valid message type: %u", msg_type);
-      this->fatal_error_();
-      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);
-  }
-}
-void APIConnection::read_message_(uint32_t size, uint32_t type, uint8_t *msg) {
-  this->last_traffic_ = millis();
-
-  switch (static_cast<APIMessageType>(type)) {
-    case APIMessageType::HELLO_REQUEST: {
-      HelloRequest req;
-      req.decode(msg, size);
-      this->on_hello_request_(req);
-      break;
-    }
-    case APIMessageType::HELLO_RESPONSE: {
-      // Invalid
-      break;
-    }
-    case APIMessageType::CONNECT_REQUEST: {
-      ConnectRequest req;
-      req.decode(msg, size);
-      this->on_connect_request_(req);
-      break;
-    }
-    case APIMessageType::CONNECT_RESPONSE:
-      // Invalid
-      break;
-    case APIMessageType::DISCONNECT_REQUEST: {
-      DisconnectRequest req;
-      req.decode(msg, size);
-      this->on_disconnect_request_(req);
-      break;
-    }
-    case APIMessageType::DISCONNECT_RESPONSE: {
-      DisconnectResponse req;
-      req.decode(msg, size);
-      this->on_disconnect_response_(req);
-      break;
-    }
-    case APIMessageType::PING_REQUEST: {
-      PingRequest req;
-      req.decode(msg, size);
-      this->on_ping_request_(req);
-      break;
-    }
-    case APIMessageType::PING_RESPONSE: {
-      PingResponse req;
-      req.decode(msg, size);
-      this->on_ping_response_(req);
-      break;
-    }
-    case APIMessageType::DEVICE_INFO_REQUEST: {
-      DeviceInfoRequest req;
-      req.decode(msg, size);
-      this->on_device_info_request_(req);
-      break;
-    }
-    case APIMessageType::DEVICE_INFO_RESPONSE: {
-      // Invalid
-      break;
-    }
-    case APIMessageType::LIST_ENTITIES_REQUEST: {
-      ListEntitiesRequest req;
-      req.decode(msg, size);
-      this->on_list_entities_request_(req);
-      break;
-    }
-    case APIMessageType::LIST_ENTITIES_BINARY_SENSOR_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_COVER_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_FAN_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_LIGHT_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_SENSOR_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_SWITCH_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_TEXT_SENSOR_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_CLIMATE_RESPONSE:
-    case APIMessageType::LIST_ENTITIES_DONE_RESPONSE:
-      // Invalid
-      break;
-    case APIMessageType::SUBSCRIBE_STATES_REQUEST: {
-      SubscribeStatesRequest req;
-      req.decode(msg, size);
-      this->on_subscribe_states_request_(req);
-      break;
-    }
-    case APIMessageType::BINARY_SENSOR_STATE_RESPONSE:
-    case APIMessageType::COVER_STATE_RESPONSE:
-    case APIMessageType::FAN_STATE_RESPONSE:
-    case APIMessageType::LIGHT_STATE_RESPONSE:
-    case APIMessageType::SENSOR_STATE_RESPONSE:
-    case APIMessageType::SWITCH_STATE_RESPONSE:
-    case APIMessageType::TEXT_SENSOR_STATE_RESPONSE:
-    case APIMessageType::CAMERA_IMAGE_RESPONSE:
-    case APIMessageType::CLIMATE_STATE_RESPONSE:
-      // Invalid
-      break;
-    case APIMessageType::SUBSCRIBE_LOGS_REQUEST: {
-      SubscribeLogsRequest req;
-      req.decode(msg, size);
-      this->on_subscribe_logs_request_(req);
-      break;
-    }
-    case APIMessageType ::SUBSCRIBE_LOGS_RESPONSE:
-      // Invalid
-      break;
-    case APIMessageType::COVER_COMMAND_REQUEST: {
-#ifdef USE_COVER
-      CoverCommandRequest req;
-      req.decode(msg, size);
-      this->on_cover_command_request_(req);
-#endif
-      break;
-    }
-    case APIMessageType::FAN_COMMAND_REQUEST: {
-#ifdef USE_FAN
-      FanCommandRequest req;
-      req.decode(msg, size);
-      this->on_fan_command_request_(req);
-#endif
-      break;
-    }
-    case APIMessageType::LIGHT_COMMAND_REQUEST: {
-#ifdef USE_LIGHT
-      LightCommandRequest req;
-      req.decode(msg, size);
-      this->on_light_command_request_(req);
-#endif
-      break;
-    }
-    case APIMessageType::SWITCH_COMMAND_REQUEST: {
-#ifdef USE_SWITCH
-      SwitchCommandRequest req;
-      req.decode(msg, size);
-      this->on_switch_command_request_(req);
-#endif
-      break;
-    }
-    case APIMessageType::CLIMATE_COMMAND_REQUEST: {
-#ifdef USE_CLIMATE
-      ClimateCommandRequest req;
-      req.decode(msg, size);
-      this->on_climate_command_request_(req);
-#endif
-      break;
-    }
-    case APIMessageType::SUBSCRIBE_SERVICE_CALLS_REQUEST: {
-      SubscribeServiceCallsRequest req;
-      req.decode(msg, size);
-      this->on_subscribe_service_calls_request_(req);
-      break;
-    }
-    case APIMessageType::SERVICE_CALL_RESPONSE:
-      // Invalid
-      break;
-    case APIMessageType::GET_TIME_REQUEST:
-      // Invalid
-      break;
-    case APIMessageType::GET_TIME_RESPONSE: {
-#ifdef USE_HOMEASSISTANT_TIME
-      homeassistant::GetTimeResponse req;
-      req.decode(msg, size);
-#endif
-      break;
-    }
-    case APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST: {
-      SubscribeHomeAssistantStatesRequest req;
-      req.decode(msg, size);
-      this->on_subscribe_home_assistant_states_request_(req);
-      break;
-    }
-    case APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE:
-      // Invalid
-      break;
-    case APIMessageType::HOME_ASSISTANT_STATE_RESPONSE: {
-      HomeAssistantStateResponse req;
-      req.decode(msg, size);
-      this->on_home_assistant_state_response_(req);
-      break;
-    }
-    case APIMessageType::EXECUTE_SERVICE_REQUEST: {
-      ExecuteServiceRequest req;
-      req.decode(msg, size);
-      this->on_execute_service_(req);
-      break;
-    }
-    case APIMessageType::CAMERA_IMAGE_REQUEST: {
-#ifdef USE_ESP32_CAMERA
-      CameraImageRequest req;
-      req.decode(msg, size);
-      this->on_camera_image_request_(req);
-#endif
-      break;
-    }
-  }
-}
-void APIConnection::on_hello_request_(const HelloRequest &req) {
-  ESP_LOGVV(TAG, "on_hello_request_(client_info='%s')", req.get_client_info().c_str());
-  this->client_info_ = req.get_client_info() + " (" + this->client_->remoteIP().toString().c_str();
-  this->client_info_ += ")";
-  ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
-
-  auto buffer = this->get_buffer();
-  // uint32 api_version_major = 1; -> 1
-  buffer.encode_uint32(1, 1);
-  // uint32 api_version_minor = 2; -> 1
-  buffer.encode_uint32(2, 1);
-
-  // string server_info = 3;
-  buffer.encode_string(3, App.get_name() + " (esphome v" ESPHOME_VERSION ")");
-  bool success = this->send_buffer(APIMessageType::HELLO_RESPONSE);
-  if (!success) {
-    this->fatal_error_();
-    return;
-  }
-
-  this->connection_state_ = ConnectionState::WAITING_FOR_CONNECT;
-}
-void APIConnection::on_connect_request_(const ConnectRequest &req) {
-  ESP_LOGVV(TAG, "on_connect_request_(password='%s')", req.get_password().c_str());
-  bool correct = this->parent_->check_password(req.get_password());
-  auto buffer = this->get_buffer();
-  // bool invalid_password = 1;
-  buffer.encode_bool(1, !correct);
-  bool success = this->send_buffer(APIMessageType::CONNECT_RESPONSE);
-  if (!success) {
-    this->fatal_error_();
-    return;
-  }
-
-  if (correct) {
-    ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
-    this->connection_state_ = ConnectionState::CONNECTED;
-
-#ifdef USE_HOMEASSISTANT_TIME
-    if (homeassistant::global_homeassistant_time != nullptr) {
-      this->send_time_request();
-    }
-#endif
-  }
-}
-void APIConnection::on_disconnect_request_(const DisconnectRequest &req) {
-  ESP_LOGVV(TAG, "on_disconnect_request_");
-  // remote initiated disconnect_client
-  if (!this->send_empty_message(APIMessageType::DISCONNECT_RESPONSE)) {
-    this->fatal_error_();
-    return;
-  }
-  this->disconnect_client();
-}
-void APIConnection::on_disconnect_response_(const DisconnectResponse &req) {
-  ESP_LOGVV(TAG, "on_disconnect_response_");
-  // we initiated disconnect_client
-  this->disconnect_client();
-}
-void APIConnection::on_ping_request_(const PingRequest &req) {
-  ESP_LOGVV(TAG, "on_ping_request_");
-  PingResponse resp;
-  this->send_message(resp);
-}
-void APIConnection::on_ping_response_(const PingResponse &req) {
-  ESP_LOGVV(TAG, "on_ping_response_");
-  // we initiated ping
-  this->sent_ping_ = false;
-}
-void APIConnection::on_device_info_request_(const DeviceInfoRequest &req) {
-  ESP_LOGVV(TAG, "on_device_info_request_");
-  auto buffer = this->get_buffer();
-  // bool uses_password = 1;
-  buffer.encode_bool(1, this->parent_->uses_password());
-  // string name = 2;
-  buffer.encode_string(2, App.get_name());
-  // string mac_address = 3;
-  buffer.encode_string(3, get_mac_address_pretty());
-  // string esphome_version = 4;
-  buffer.encode_string(4, ESPHOME_VERSION);
-  // string compilation_time = 5;
-  buffer.encode_string(5, App.get_compilation_time());
-#ifdef ARDUINO_BOARD
-  // string model = 6;
-  buffer.encode_string(6, ARDUINO_BOARD);
-#endif
-#ifdef USE_DEEP_SLEEP
-  // bool has_deep_sleep = 7;
-  buffer.encode_bool(7, deep_sleep::global_has_deep_sleep);
-#endif
-  this->send_buffer(APIMessageType::DEVICE_INFO_RESPONSE);
-}
-void APIConnection::on_list_entities_request_(const ListEntitiesRequest &req) {
-  ESP_LOGVV(TAG, "on_list_entities_request_");
-  this->list_entities_iterator_.begin();
-}
-void APIConnection::on_subscribe_states_request_(const SubscribeStatesRequest &req) {
-  ESP_LOGVV(TAG, "on_subscribe_states_request_");
-  this->state_subscription_ = true;
-  this->initial_state_iterator_.begin();
-}
-void APIConnection::on_subscribe_logs_request_(const SubscribeLogsRequest &req) {
-  ESP_LOGVV(TAG, "on_subscribe_logs_request_");
-  this->log_subscription_ = req.get_level();
-  if (req.get_dump_config()) {
-    App.schedule_dump_config();
-  }
-}
-
-void APIConnection::fatal_error_() {
-  this->client_->close();
-  this->remove_ = true;
-}
-bool APIConnection::valid_rx_message_type_(uint32_t type) {
-  switch (static_cast<APIMessageType>(type)) {
-    case APIMessageType::HELLO_RESPONSE:
-    case APIMessageType::CONNECT_RESPONSE:
-      return false;
-    case APIMessageType::HELLO_REQUEST:
-      return this->connection_state_ == ConnectionState::WAITING_FOR_HELLO;
-    case APIMessageType::CONNECT_REQUEST:
-      return this->connection_state_ == ConnectionState::WAITING_FOR_CONNECT;
-    case APIMessageType::PING_REQUEST:
-    case APIMessageType::PING_RESPONSE:
-    case APIMessageType::DISCONNECT_REQUEST:
-    case APIMessageType::DISCONNECT_RESPONSE:
-    case APIMessageType::DEVICE_INFO_REQUEST:
-      if (this->connection_state_ == ConnectionState::WAITING_FOR_CONNECT)
-        return true;
-    default:
-      return this->connection_state_ == ConnectionState::CONNECTED;
-  }
-}
-bool APIConnection::send_message(APIMessage &msg) {
-  this->send_buffer_.clear();
-  APIBuffer buf(&this->send_buffer_);
-  msg.encode(buf);
-  return this->send_buffer(msg.message_type());
-}
-bool APIConnection::send_empty_message(APIMessageType type) {
-  this->send_buffer_.clear();
-  return this->send_buffer(type);
-}
-
-void APIConnection::disconnect_client() {
-  this->client_->close();
-  this->remove_ = true;
-}
-void encode_varint(uint8_t *dat, uint8_t *len, uint32_t value) {
-  if (value <= 0x7F) {
-    *dat = value;
-    (*len)++;
-    return;
-  }
-
-  while (value) {
-    uint8_t temp = value & 0x7F;
-    value >>= 7;
-    if (value) {
-      *dat = temp | 0x80;
-    } else {
-      *dat = temp;
-    }
-    dat++;
-    (*len)++;
-  }
-}
-
-bool APIConnection::send_buffer(APIMessageType type) {
-  uint8_t header[20];
-  header[0] = 0x00;
-  uint8_t header_len = 1;
-  encode_varint(header + header_len, &header_len, this->send_buffer_.size());
-  encode_varint(header + header_len, &header_len, static_cast<uint32_t>(type));
-
-  size_t needed_space = this->send_buffer_.size() + header_len;
-
-  if (needed_space > this->client_->space()) {
-    delay(0);
-    if (needed_space > this->client_->space()) {
-      if (type != APIMessageType::SUBSCRIBE_LOGS_RESPONSE) {
-        ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
-      }
-      delay(0);
-      return false;
-    }
-  }
-
-  //  char buffer[512];
-  //  uint32_t offset = 0;
-  //  for (int j = 0; j < header_len; j++) {
-  //    offset += snprintf(buffer + offset, 512 - offset, "0x%02X ", header[j]);
-  //  }
-  //  offset += snprintf(buffer + offset, 512 - offset, "| ");
-  //  for (auto &it : this->send_buffer_) {
-  //    int i = snprintf(buffer + offset, 512 - offset, "0x%02X ", it);
-  //    if (i <= 0)
-  //      break;
-  //    offset += i;
-  //  }
-  //  ESP_LOGVV(TAG, "SEND %s", buffer);
-
-  this->client_->add(reinterpret_cast<char *>(header), header_len);
-  this->client_->add(reinterpret_cast<char *>(this->send_buffer_.data()), this->send_buffer_.size());
-  return this->client_->send();
-}
-
-void APIConnection::loop() {
-  if (!network_is_connected()) {
-    // when network is disconnected force disconnect immediately
-    // don't wait for timeout
-    this->fatal_error_();
-    return;
-  }
-  if (this->client_->disconnected()) {
-    // failsafe for disconnect logic
-    this->on_disconnect_();
-    return;
-  }
-  this->parse_recv_buffer_();
-
-  this->list_entities_iterator_.advance();
-  this->initial_state_iterator_.advance();
-
-  const uint32_t keepalive = 60000;
-  if (this->sent_ping_) {
-    if (millis() - this->last_traffic_ > (keepalive * 3) / 2) {
-      ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
-      this->disconnect_client();
-    }
-  } else if (millis() - this->last_traffic_ > keepalive) {
-    this->sent_ping_ = true;
-    this->send_ping_request();
-  }
-
-#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->get_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(APIMessageType::CAMERA_IMAGE_RESPONSE);
-      if (success) {
-        this->image_reader_.consume_data(to_send);
-      }
-      if (success && done) {
-        this->image_reader_.return_image();
-      }
-    }
-  }
-#endif
-}
-
-#ifdef USE_BINARY_SENSOR
-bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, binary_sensor->get_object_id_hash());
-  // bool state = 2;
-  buffer.encode_bool(2, state);
-  return this->send_buffer(APIMessageType::BINARY_SENSOR_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_COVER
-bool APIConnection::send_cover_state(cover::Cover *cover) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  auto traits = cover->get_traits();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, cover->get_object_id_hash());
-  // enum LegacyCoverState {
-  //   OPEN = 0;
-  //   CLOSED = 1;
-  // }
-  // LegacyCoverState legacy_state = 2;
-  uint32_t state = (cover->position == cover::COVER_OPEN) ? 0 : 1;
-  buffer.encode_uint32(2, state);
-  // float position = 3;
-  buffer.encode_float(3, cover->position);
-  if (traits.get_supports_tilt()) {
-    // float tilt = 4;
-    buffer.encode_float(4, cover->tilt);
-  }
-  // enum CoverCurrentOperation {
-  //   IDLE = 0;
-  //   IS_OPENING = 1;
-  //   IS_CLOSING = 2;
-  // }
-  // CoverCurrentOperation current_operation = 5;
-  buffer.encode_uint32(5, cover->current_operation);
-  return this->send_buffer(APIMessageType::COVER_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_FAN
-bool APIConnection::send_fan_state(fan::FanState *fan) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, fan->get_object_id_hash());
-  // bool state = 2;
-  buffer.encode_bool(2, fan->state);
-  // bool oscillating = 3;
-  if (fan->get_traits().supports_oscillation()) {
-    buffer.encode_bool(3, fan->oscillating);
-  }
-  // enum FanSpeed {
-  //   LOW = 0;
-  //   MEDIUM = 1;
-  //   HIGH = 2;
-  // }
-  // FanSpeed speed = 4;
-  if (fan->get_traits().supports_speed()) {
-    buffer.encode_uint32(4, fan->speed);
-  }
-  return this->send_buffer(APIMessageType::FAN_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_LIGHT
-bool APIConnection::send_light_state(light::LightState *light) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  auto traits = light->get_traits();
-  auto values = light->remote_values;
-
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, light->get_object_id_hash());
-  // bool state = 2;
-  buffer.encode_bool(2, values.get_state() != 0.0f);
-  // float brightness = 3;
-  if (traits.get_supports_brightness()) {
-    buffer.encode_float(3, values.get_brightness());
-  }
-  if (traits.get_supports_rgb()) {
-    // float red = 4;
-    buffer.encode_float(4, values.get_red());
-    // float green = 5;
-    buffer.encode_float(5, values.get_green());
-    // float blue = 6;
-    buffer.encode_float(6, values.get_blue());
-  }
-  // float white = 7;
-  if (traits.get_supports_rgb_white_value()) {
-    buffer.encode_float(7, values.get_white());
-  }
-  // float color_temperature = 8;
-  if (traits.get_supports_color_temperature()) {
-    buffer.encode_float(8, values.get_color_temperature());
-  }
-  // string effect = 9;
-  if (light->supports_effects()) {
-    buffer.encode_string(9, light->get_effect_name());
-  }
-  return this->send_buffer(APIMessageType::LIGHT_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_SENSOR
-bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, sensor->get_object_id_hash());
-  // float state = 2;
-  buffer.encode_float(2, state);
-  return this->send_buffer(APIMessageType::SENSOR_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_SWITCH
-bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, a_switch->get_object_id_hash());
-  // bool state = 2;
-  buffer.encode_bool(2, state);
-  return this->send_buffer(APIMessageType::SWITCH_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_TEXT_SENSOR
-bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, text_sensor->get_object_id_hash());
-  // string state = 2;
-  buffer.encode_string(2, state);
-  return this->send_buffer(APIMessageType::TEXT_SENSOR_STATE_RESPONSE);
-}
-#endif
-
-#ifdef USE_CLIMATE
-bool APIConnection::send_climate_state(climate::Climate *climate) {
-  if (!this->state_subscription_)
-    return false;
-
-  auto buffer = this->get_buffer();
-  auto traits = climate->get_traits();
-  // fixed32 key = 1;
-  buffer.encode_fixed32(1, climate->get_object_id_hash());
-  // ClimateMode mode = 2;
-  buffer.encode_uint32(2, static_cast<uint32_t>(climate->mode));
-  // float current_temperature = 3;
-  if (traits.get_supports_current_temperature()) {
-    buffer.encode_float(3, climate->current_temperature);
-  }
-  if (traits.get_supports_two_point_target_temperature()) {
-    // float target_temperature_low = 5;
-    buffer.encode_float(5, climate->target_temperature_low);
-    // float target_temperature_high = 6;
-    buffer.encode_float(6, climate->target_temperature_high);
-  } else {
-    // float target_temperature = 4;
-    buffer.encode_float(4, climate->target_temperature);
-  }
-  // bool away = 7;
-  if (traits.get_supports_away()) {
-    buffer.encode_bool(7, climate->away);
-  }
-  return this->send_buffer(APIMessageType::CLIMATE_STATE_RESPONSE);
-}
-#endif
-
-bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
-  if (this->log_subscription_ < level)
-    return false;
-
-  auto buffer = this->get_buffer();
-  // LogLevel level = 1;
-  buffer.encode_uint32(1, static_cast<uint32_t>(level));
-  // string tag = 2;
-  // buffer.encode_string(2, tag, strlen(tag));
-  // string message = 3;
-  buffer.encode_string(3, line, strlen(line));
-  bool success = this->send_buffer(APIMessageType::SUBSCRIBE_LOGS_RESPONSE);
-
-  if (!success) {
-    buffer = this->get_buffer();
-    // bool send_failed = 4;
-    buffer.encode_bool(4, true);
-    return this->send_buffer(APIMessageType::SUBSCRIBE_LOGS_RESPONSE);
-  } else {
-    return true;
-  }
-}
-bool APIConnection::send_disconnect_request() {
-  DisconnectRequest req;
-  return this->send_message(req);
-}
-bool APIConnection::send_ping_request() {
-  ESP_LOGVV(TAG, "Sending ping...");
-  PingRequest req;
-  return this->send_message(req);
-}
-
-#ifdef USE_COVER
-void APIConnection::on_cover_command_request_(const CoverCommandRequest &req) {
-  ESP_LOGVV(TAG, "on_cover_command_request_");
-  cover::Cover *cover = App.get_cover_by_key(req.get_key());
-  if (cover == nullptr)
-    return;
-
-  auto call = cover->make_call();
-  if (req.get_legacy_command().has_value()) {
-    auto cmd = *req.get_legacy_command();
-    switch (cmd) {
-      case LEGACY_COVER_COMMAND_OPEN:
-        call.set_command_open();
-        break;
-      case LEGACY_COVER_COMMAND_CLOSE:
-        call.set_command_close();
-        break;
-      case LEGACY_COVER_COMMAND_STOP:
-        call.set_command_stop();
-        break;
-    }
-  }
-  if (req.get_position().has_value()) {
-    auto pos = *req.get_position();
-    call.set_position(pos);
-  }
-  if (req.get_tilt().has_value()) {
-    auto tilt = *req.get_tilt();
-    call.set_tilt(tilt);
-  }
-  if (req.get_stop()) {
-    call.set_command_stop();
-  }
-  call.perform();
-}
-#endif
-
-#ifdef USE_FAN
-void APIConnection::on_fan_command_request_(const FanCommandRequest &req) {
-  ESP_LOGVV(TAG, "on_fan_command_request_");
-  fan::FanState *fan = App.get_fan_by_key(req.get_key());
-  if (fan == nullptr)
-    return;
-
-  auto call = fan->make_call();
-  call.set_state(req.get_state());
-  call.set_oscillating(req.get_oscillating());
-  call.set_speed(req.get_speed());
-  call.perform();
-}
-#endif
-
-#ifdef USE_LIGHT
-void APIConnection::on_light_command_request_(const LightCommandRequest &req) {
-  ESP_LOGVV(TAG, "on_light_command_request_");
-  light::LightState *light = App.get_light_by_key(req.get_key());
-  if (light == nullptr)
-    return;
-
-  auto call = light->make_call();
-  call.set_state(req.get_state());
-  call.set_brightness(req.get_brightness());
-  call.set_red(req.get_red());
-  call.set_green(req.get_green());
-  call.set_blue(req.get_blue());
-  call.set_white(req.get_white());
-  call.set_color_temperature(req.get_color_temperature());
-  call.set_transition_length(req.get_transition_length());
-  call.set_flash_length(req.get_flash_length());
-  call.set_effect(req.get_effect());
-  call.perform();
-}
-#endif
-
-#ifdef USE_SWITCH
-void APIConnection::on_switch_command_request_(const SwitchCommandRequest &req) {
-  ESP_LOGVV(TAG, "on_switch_command_request_");
-  switch_::Switch *a_switch = App.get_switch_by_key(req.get_key());
-  if (a_switch == nullptr || a_switch->is_internal())
-    return;
-
-  if (req.get_state()) {
-    a_switch->turn_on();
-  } else {
-    a_switch->turn_off();
-  }
-}
-#endif
-
-#ifdef USE_CLIMATE
-void APIConnection::on_climate_command_request_(const ClimateCommandRequest &req) {
-  ESP_LOGVV(TAG, "on_climate_command_request_");
-  climate::Climate *climate = App.get_climate_by_key(req.get_key());
-  if (climate == nullptr)
-    return;
-
-  auto call = climate->make_call();
-  if (req.get_mode().has_value())
-    call.set_mode(*req.get_mode());
-  if (req.get_target_temperature().has_value())
-    call.set_target_temperature(*req.get_target_temperature());
-  if (req.get_target_temperature_low().has_value())
-    call.set_target_temperature_low(*req.get_target_temperature_low());
-  if (req.get_target_temperature_high().has_value())
-    call.set_target_temperature_high(*req.get_target_temperature_high());
-  if (req.get_away().has_value())
-    call.set_away(*req.get_away());
-  call.perform();
-}
-#endif
-
-void APIConnection::on_subscribe_service_calls_request_(const SubscribeServiceCallsRequest &req) {
-  this->service_call_subscription_ = true;
-}
-void APIConnection::send_service_call(ServiceCallResponse &call) {
-  if (!this->service_call_subscription_)
-    return;
-
-  this->send_message(call);
-}
-void APIConnection::on_subscribe_home_assistant_states_request_(const SubscribeHomeAssistantStatesRequest &req) {
-  for (auto &it : this->parent_->get_state_subs()) {
-    auto buffer = this->get_buffer();
-    // string entity_id = 1;
-    buffer.encode_string(1, it.entity_id);
-    this->send_buffer(APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE);
-  }
-}
-void APIConnection::on_home_assistant_state_response_(const HomeAssistantStateResponse &req) {
-  for (auto &it : this->parent_->get_state_subs()) {
-    if (it.entity_id == req.get_entity_id()) {
-      it.callback(req.get_state());
-    }
-  }
-}
-void APIConnection::on_execute_service_(const ExecuteServiceRequest &req) {
-  ESP_LOGVV(TAG, "on_execute_service_");
-  bool found = false;
-  for (auto *service : this->parent_->get_user_services()) {
-    if (service->execute_service(req)) {
-      found = true;
-    }
-  }
-  if (!found) {
-    ESP_LOGV(TAG, "Could not find matching service!");
-  }
-}
-
-APIBuffer APIConnection::get_buffer() {
-  this->send_buffer_.clear();
-  return {&this->send_buffer_};
-}
-#ifdef USE_HOMEASSISTANT_TIME
-void APIConnection::send_time_request() { this->send_empty_message(APIMessageType::GET_TIME_REQUEST); }
-#endif
-
-#ifdef USE_ESP32_CAMERA
-void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
-  if (!this->state_subscription_)
-    return;
-  if (this->image_reader_.available())
-    return;
-  this->image_reader_.set_image(image);
-}
-#endif
-
-#ifdef USE_ESP32_CAMERA
-void APIConnection::on_camera_image_request_(const CameraImageRequest &req) {
-  if (esp32_camera::global_esp32_camera == nullptr)
-    return;
-
-  ESP_LOGV(TAG, "on_camera_image_request_ stream=%s single=%s", YESNO(req.get_stream()), YESNO(req.get_single()));
-  if (req.get_single()) {
-    esp32_camera::global_esp32_camera->request_image();
-  }
-  if (req.get_stream()) {
-    esp32_camera::global_esp32_camera->request_stream();
-  }
-}
-#endif
-
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index be75d87264..db826c55c2 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -4,14 +4,12 @@
 #include "esphome/core/controller.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/log.h"
+#include "api_pb2.h"
+#include "api_pb2_service.h"
 #include "util.h"
-#include "api_message.h"
-#include "basic_messages.h"
 #include "list_entities.h"
 #include "subscribe_state.h"
-#include "subscribe_logs.h"
-#include "command_messages.h"
-#include "service_call_message.h"
+#include "homeassistant_service.h"
 #include "user_services.h"
 
 #ifdef ARDUINO_ARCH_ESP32
@@ -24,130 +22,6 @@
 namespace esphome {
 namespace api {
 
-class APIServer;
-
-class APIConnection {
- public:
-  APIConnection(AsyncClient *client, APIServer *parent);
-  ~APIConnection();
-
-  void disconnect_client();
-  APIBuffer get_buffer();
-  bool send_buffer(APIMessageType type);
-  bool send_message(APIMessage &msg);
-  bool send_empty_message(APIMessageType type);
-  void loop();
-
-#ifdef USE_BINARY_SENSOR
-  bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
-#endif
-#ifdef USE_COVER
-  bool send_cover_state(cover::Cover *cover);
-#endif
-#ifdef USE_FAN
-  bool send_fan_state(fan::FanState *fan);
-#endif
-#ifdef USE_LIGHT
-  bool send_light_state(light::LightState *light);
-#endif
-#ifdef USE_SENSOR
-  bool send_sensor_state(sensor::Sensor *sensor, float state);
-#endif
-#ifdef USE_SWITCH
-  bool send_switch_state(switch_::Switch *a_switch, bool state);
-#endif
-#ifdef USE_TEXT_SENSOR
-  bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
-#endif
-#ifdef USE_ESP32_CAMERA
-  void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
-#endif
-#ifdef USE_CLIMATE
-  bool send_climate_state(climate::Climate *climate);
-#endif
-  bool send_log_message(int level, const char *tag, const char *line);
-  bool send_disconnect_request();
-  bool send_ping_request();
-  void send_service_call(ServiceCallResponse &call);
-#ifdef USE_HOMEASSISTANT_TIME
-  void send_time_request();
-#endif
-
- 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 fatal_error_();
-  bool valid_rx_message_type_(uint32_t msg_type);
-  void read_message_(uint32_t size, uint32_t type, uint8_t *msg);
-  void parse_recv_buffer_();
-
-  // request types
-  void on_hello_request_(const HelloRequest &req);
-  void on_connect_request_(const ConnectRequest &req);
-  void on_disconnect_request_(const DisconnectRequest &req);
-  void on_disconnect_response_(const DisconnectResponse &req);
-  void on_ping_request_(const PingRequest &req);
-  void on_ping_response_(const PingResponse &req);
-  void on_device_info_request_(const DeviceInfoRequest &req);
-  void on_list_entities_request_(const ListEntitiesRequest &req);
-  void on_subscribe_states_request_(const SubscribeStatesRequest &req);
-  void on_subscribe_logs_request_(const SubscribeLogsRequest &req);
-#ifdef USE_COVER
-  void on_cover_command_request_(const CoverCommandRequest &req);
-#endif
-#ifdef USE_FAN
-  void on_fan_command_request_(const FanCommandRequest &req);
-#endif
-#ifdef USE_LIGHT
-  void on_light_command_request_(const LightCommandRequest &req);
-#endif
-#ifdef USE_SWITCH
-  void on_switch_command_request_(const SwitchCommandRequest &req);
-#endif
-#ifdef USE_CLIMATE
-  void on_climate_command_request_(const ClimateCommandRequest &req);
-#endif
-  void on_subscribe_service_calls_request_(const SubscribeServiceCallsRequest &req);
-  void on_subscribe_home_assistant_states_request_(const SubscribeHomeAssistantStatesRequest &req);
-  void on_home_assistant_state_response_(const HomeAssistantStateResponse &req);
-  void on_execute_service_(const ExecuteServiceRequest &req);
-#ifdef USE_ESP32_CAMERA
-  void on_camera_image_request_(const CameraImageRequest &req);
-#endif
-
-  enum class ConnectionState {
-    WAITING_FOR_HELLO,
-    WAITING_FOR_CONNECT,
-    CONNECTED,
-  } connection_state_{ConnectionState::WAITING_FOR_HELLO};
-
-  bool remove_{false};
-
-  std::vector<uint8_t> send_buffer_;
-  std::vector<uint8_t> recv_buffer_;
-
-  std::string client_info_;
-#ifdef USE_ESP32_CAMERA
-  esp32_camera::CameraImageReader image_reader_;
-#endif
-
-  bool state_subscription_{false};
-  int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
-  uint32_t last_traffic_;
-  bool sent_ping_{false};
-  bool service_call_subscription_{false};
-  AsyncClient *client_;
-  APIServer *parent_;
-  InitialStateIterator initial_state_iterator_;
-  ListEntitiesIterator list_entities_iterator_;
-};
-
-template<typename... Ts> class HomeAssistantServiceCallAction;
-
 class APIServer : public Component, public Controller {
  public:
   APIServer();
@@ -187,7 +61,7 @@ class APIServer : public Component, public Controller {
 #ifdef USE_CLIMATE
   void on_climate_update(climate::Climate *obj) override;
 #endif
-  void send_service_call(ServiceCallResponse &call);
+  void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 #ifdef USE_HOMEASSISTANT_TIME
   void request_time();
@@ -217,22 +91,6 @@ class APIServer : public Component, public Controller {
 
 extern APIServer *global_api_server;
 
-template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
- public:
-  explicit HomeAssistantServiceCallAction(APIServer *parent) : parent_(parent) {}
-  void set_service(const std::string &service) { this->resp_.set_service(service); }
-  void set_data(const std::vector<KeyValuePair> &data) { this->resp_.set_data(data); }
-  void set_data_template(const std::vector<KeyValuePair> &data_template) {
-    this->resp_.set_data_template(data_template);
-  }
-  void set_variables(const std::vector<TemplatableKeyValuePair> &variables) { this->resp_.set_variables(variables); }
-  void play(Ts... x) override { this->parent_->send_service_call(this->resp_); }
-
- protected:
-  APIServer *parent_;
-  ServiceCallResponse resp_;
-};
-
 template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
  public:
   bool check(Ts... x) override { return global_api_server->is_connected(); }
diff --git a/esphome/components/api/basic_messages.cpp b/esphome/components/api/basic_messages.cpp
deleted file mode 100644
index 71e0045c05..0000000000
--- a/esphome/components/api/basic_messages.cpp
+++ /dev/null
@@ -1,57 +0,0 @@
-#include "basic_messages.h"
-#include "esphome/core/log.h"
-
-namespace esphome {
-namespace api {
-
-// Hello
-bool HelloRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 1:  // string client_info = 1;
-      this->client_info_ = as_string(value, len);
-      return true;
-    default:
-      return false;
-  }
-}
-const std::string &HelloRequest::get_client_info() const { return this->client_info_; }
-void HelloRequest::set_client_info(const std::string &client_info) { this->client_info_ = client_info; }
-APIMessageType HelloRequest::message_type() const { return APIMessageType::HELLO_REQUEST; }
-
-// Connect
-bool ConnectRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 1:  // string password = 1;
-      this->password_ = as_string(value, len);
-      return true;
-    default:
-      return false;
-  }
-}
-const std::string &ConnectRequest::get_password() const { return this->password_; }
-void ConnectRequest::set_password(const std::string &password) { this->password_ = password; }
-APIMessageType ConnectRequest::message_type() const { return APIMessageType::CONNECT_REQUEST; }
-
-APIMessageType DeviceInfoRequest::message_type() const { return APIMessageType::DEVICE_INFO_REQUEST; }
-APIMessageType DisconnectRequest::message_type() const { return APIMessageType::DISCONNECT_REQUEST; }
-bool DisconnectRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 1:  // string reason = 1;
-      this->reason_ = as_string(value, len);
-      return true;
-    default:
-      return false;
-  }
-}
-const std::string &DisconnectRequest::get_reason() const { return this->reason_; }
-void DisconnectRequest::set_reason(const std::string &reason) { this->reason_ = reason; }
-void DisconnectRequest::encode(APIBuffer &buffer) {
-  // string reason = 1;
-  buffer.encode_string(1, this->reason_);
-}
-APIMessageType DisconnectResponse::message_type() const { return APIMessageType::DISCONNECT_RESPONSE; }
-APIMessageType PingRequest::message_type() const { return APIMessageType::PING_REQUEST; }
-APIMessageType PingResponse::message_type() const { return APIMessageType::PING_RESPONSE; }
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/basic_messages.h b/esphome/components/api/basic_messages.h
deleted file mode 100644
index 213a053514..0000000000
--- a/esphome/components/api/basic_messages.h
+++ /dev/null
@@ -1,63 +0,0 @@
-#pragma once
-
-#include "api_message.h"
-
-namespace esphome {
-namespace api {
-
-class HelloRequest : public APIMessage {
- public:
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  const std::string &get_client_info() const;
-  void set_client_info(const std::string &client_info);
-  APIMessageType message_type() const override;
-
- protected:
-  std::string client_info_;
-};
-
-class ConnectRequest : public APIMessage {
- public:
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  const std::string &get_password() const;
-  void set_password(const std::string &password);
-  APIMessageType message_type() const override;
-
- protected:
-  std::string password_;
-};
-
-class DeviceInfoRequest : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
-class DisconnectRequest : public APIMessage {
- public:
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  void encode(APIBuffer &buffer) override;
-  APIMessageType message_type() const override;
-  const std::string &get_reason() const;
-  void set_reason(const std::string &reason);
-
- protected:
-  std::string reason_;
-};
-
-class DisconnectResponse : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
-class PingRequest : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
-class PingResponse : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/command_messages.cpp b/esphome/components/api/command_messages.cpp
deleted file mode 100644
index 240bc05cb7..0000000000
--- a/esphome/components/api/command_messages.cpp
+++ /dev/null
@@ -1,417 +0,0 @@
-#include "command_messages.h"
-#include "esphome/core/log.h"
-
-namespace esphome {
-namespace api {
-
-#ifdef USE_COVER
-bool CoverCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 2:
-      // bool has_legacy_command = 2;
-      this->has_legacy_command_ = value;
-      return true;
-    case 3:
-      // enum LegacyCoverCommand {
-      //   OPEN = 0;
-      //   CLOSE = 1;
-      //   STOP = 2;
-      // }
-      // LegacyCoverCommand legacy_command_ = 3;
-      this->legacy_command_ = static_cast<LegacyCoverCommand>(value);
-      return true;
-    case 4:
-      // bool has_position = 4;
-      this->has_position_ = value;
-      return true;
-    case 6:
-      // bool has_tilt = 6;
-      this->has_tilt_ = value;
-      return true;
-    case 8:
-      // bool stop = 8;
-      this->stop_ = value;
-    default:
-      return false;
-  }
-}
-bool CoverCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // fixed32 key = 1;
-      this->key_ = value;
-      return true;
-    case 5:
-      // float position = 5;
-      this->position_ = as_float(value);
-      return true;
-    case 7:
-      // float tilt = 7;
-      this->tilt_ = as_float(value);
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType CoverCommandRequest::message_type() const { return APIMessageType ::COVER_COMMAND_REQUEST; }
-uint32_t CoverCommandRequest::get_key() const { return this->key_; }
-optional<LegacyCoverCommand> CoverCommandRequest::get_legacy_command() const {
-  if (!this->has_legacy_command_)
-    return {};
-  return this->legacy_command_;
-}
-optional<float> CoverCommandRequest::get_position() const {
-  if (!this->has_position_)
-    return {};
-  return this->position_;
-}
-optional<float> CoverCommandRequest::get_tilt() const {
-  if (!this->has_tilt_)
-    return {};
-  return this->tilt_;
-}
-#endif
-
-#ifdef USE_FAN
-bool FanCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 2:
-      // bool has_state = 2;
-      this->has_state_ = value;
-      return true;
-    case 3:
-      // bool state = 3;
-      this->state_ = value;
-      return true;
-    case 4:
-      // bool has_speed = 4;
-      this->has_speed_ = value;
-      return true;
-    case 5:
-      // FanSpeed speed = 5;
-      this->speed_ = static_cast<fan::FanSpeed>(value);
-      return true;
-    case 6:
-      // bool has_oscillating = 6;
-      this->has_oscillating_ = value;
-      return true;
-    case 7:
-      // bool oscillating = 7;
-      this->oscillating_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-bool FanCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // fixed32 key = 1;
-      this->key_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType FanCommandRequest::message_type() const { return APIMessageType::FAN_COMMAND_REQUEST; }
-uint32_t FanCommandRequest::get_key() const { return this->key_; }
-optional<bool> FanCommandRequest::get_state() const {
-  if (!this->has_state_)
-    return {};
-  return this->state_;
-}
-optional<fan::FanSpeed> FanCommandRequest::get_speed() const {
-  if (!this->has_speed_)
-    return {};
-  return this->speed_;
-}
-optional<bool> FanCommandRequest::get_oscillating() const {
-  if (!this->has_oscillating_)
-    return {};
-  return this->oscillating_;
-}
-#endif
-
-#ifdef USE_LIGHT
-bool LightCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 2:
-      // bool has_state = 2;
-      this->has_state_ = value;
-      return true;
-    case 3:
-      // bool state = 3;
-      this->state_ = value;
-      return true;
-    case 4:
-      // bool has_brightness = 4;
-      this->has_brightness_ = value;
-      return true;
-    case 6:
-      // bool has_rgb = 6;
-      this->has_rgb_ = value;
-      return true;
-    case 10:
-      // bool has_white = 10;
-      this->has_white_ = value;
-      return true;
-    case 12:
-      // bool has_color_temperature = 12;
-      this->has_color_temperature_ = value;
-      return true;
-    case 14:
-      // bool has_transition_length = 14;
-      this->has_transition_length_ = value;
-      return true;
-    case 15:
-      // uint32 transition_length = 15;
-      this->transition_length_ = value;
-      return true;
-    case 16:
-      // bool has_flash_length = 16;
-      this->has_flash_length_ = value;
-      return true;
-    case 17:
-      // uint32 flash_length = 17;
-      this->flash_length_ = value;
-      return true;
-    case 18:
-      // bool has_effect = 18;
-      this->has_effect_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-bool LightCommandRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 19:
-      // string effect = 19;
-      this->effect_ = as_string(value, len);
-      return true;
-    default:
-      return false;
-  }
-}
-bool LightCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // fixed32 key = 1;
-      this->key_ = value;
-      return true;
-    case 5:
-      // float brightness = 5;
-      this->brightness_ = as_float(value);
-      return true;
-    case 7:
-      // float red = 7;
-      this->red_ = as_float(value);
-      return true;
-    case 8:
-      // float green = 8;
-      this->green_ = as_float(value);
-      return true;
-    case 9:
-      // float blue = 9;
-      this->blue_ = as_float(value);
-      return true;
-    case 11:
-      // float white = 11;
-      this->white_ = as_float(value);
-      return true;
-    case 13:
-      // float color_temperature = 13;
-      this->color_temperature_ = as_float(value);
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType LightCommandRequest::message_type() const { return APIMessageType::LIGHT_COMMAND_REQUEST; }
-uint32_t LightCommandRequest::get_key() const { return this->key_; }
-optional<bool> LightCommandRequest::get_state() const {
-  if (!this->has_state_)
-    return {};
-  return this->state_;
-}
-optional<float> LightCommandRequest::get_brightness() const {
-  if (!this->has_brightness_)
-    return {};
-  return this->brightness_;
-}
-optional<float> LightCommandRequest::get_red() const {
-  if (!this->has_rgb_)
-    return {};
-  return this->red_;
-}
-optional<float> LightCommandRequest::get_green() const {
-  if (!this->has_rgb_)
-    return {};
-  return this->green_;
-}
-optional<float> LightCommandRequest::get_blue() const {
-  if (!this->has_rgb_)
-    return {};
-  return this->blue_;
-}
-optional<float> LightCommandRequest::get_white() const {
-  if (!this->has_white_)
-    return {};
-  return this->white_;
-}
-optional<float> LightCommandRequest::get_color_temperature() const {
-  if (!this->has_color_temperature_)
-    return {};
-  return this->color_temperature_;
-}
-optional<uint32_t> LightCommandRequest::get_transition_length() const {
-  if (!this->has_transition_length_)
-    return {};
-  return this->transition_length_;
-}
-optional<uint32_t> LightCommandRequest::get_flash_length() const {
-  if (!this->has_flash_length_)
-    return {};
-  return this->flash_length_;
-}
-optional<std::string> LightCommandRequest::get_effect() const {
-  if (!this->has_effect_)
-    return {};
-  return this->effect_;
-}
-#endif
-
-#ifdef USE_SWITCH
-bool SwitchCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 2:
-      // bool state = 2;
-      this->state_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-bool SwitchCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // fixed32 key = 1;
-      this->key_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType SwitchCommandRequest::message_type() const { return APIMessageType::SWITCH_COMMAND_REQUEST; }
-uint32_t SwitchCommandRequest::get_key() const { return this->key_; }
-bool SwitchCommandRequest::get_state() const { return this->state_; }
-#endif
-
-#ifdef USE_ESP32_CAMERA
-bool CameraImageRequest::get_single() const { return this->single_; }
-bool CameraImageRequest::get_stream() const { return this->stream_; }
-bool CameraImageRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // bool single = 1;
-      this->single_ = value;
-      return true;
-    case 2:
-      // bool stream = 2;
-      this->stream_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType CameraImageRequest::message_type() const { return APIMessageType::CAMERA_IMAGE_REQUEST; }
-#endif
-
-#ifdef USE_CLIMATE
-bool ClimateCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 2:
-      // bool has_mode = 2;
-      this->has_mode_ = value;
-      return true;
-    case 3:
-      // ClimateMode mode = 3;
-      this->mode_ = static_cast<climate::ClimateMode>(value);
-      return true;
-    case 4:
-      // bool has_target_temperature = 4;
-      this->has_target_temperature_ = value;
-      return true;
-    case 6:
-      // bool has_target_temperature_low = 6;
-      this->has_target_temperature_low_ = value;
-      return true;
-    case 8:
-      // bool has_target_temperature_high = 8;
-      this->has_target_temperature_high_ = value;
-      return true;
-    case 10:
-      // bool has_away = 10;
-      this->has_away_ = value;
-      return true;
-    case 11:
-      // bool away = 11;
-      this->away_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-bool ClimateCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // fixed32 key = 1;
-      this->key_ = value;
-      return true;
-    case 5:
-      // float target_temperature = 5;
-      this->target_temperature_ = as_float(value);
-      return true;
-    case 7:
-      // float target_temperature_low = 7;
-      this->target_temperature_low_ = as_float(value);
-      return true;
-    case 9:
-      // float target_temperature_high = 9;
-      this->target_temperature_high_ = as_float(value);
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType ClimateCommandRequest::message_type() const { return APIMessageType::CLIMATE_COMMAND_REQUEST; }
-uint32_t ClimateCommandRequest::get_key() const { return this->key_; }
-optional<climate::ClimateMode> ClimateCommandRequest::get_mode() const {
-  if (!this->has_mode_)
-    return {};
-  return this->mode_;
-}
-optional<float> ClimateCommandRequest::get_target_temperature() const {
-  if (!this->has_target_temperature_)
-    return {};
-  return this->target_temperature_;
-}
-optional<float> ClimateCommandRequest::get_target_temperature_low() const {
-  if (!this->has_target_temperature_low_)
-    return {};
-  return this->target_temperature_low_;
-}
-optional<float> ClimateCommandRequest::get_target_temperature_high() const {
-  if (!this->has_target_temperature_high_)
-    return {};
-  return this->target_temperature_high_;
-}
-optional<bool> ClimateCommandRequest::get_away() const {
-  if (!this->has_away_)
-    return {};
-  return this->away_;
-}
-#endif
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/command_messages.h b/esphome/components/api/command_messages.h
deleted file mode 100644
index 09ca9cb126..0000000000
--- a/esphome/components/api/command_messages.h
+++ /dev/null
@@ -1,162 +0,0 @@
-#pragma once
-
-#include "esphome/core/component.h"
-#include "esphome/core/defines.h"
-#include "api_message.h"
-
-namespace esphome {
-namespace api {
-
-#ifdef USE_COVER
-enum LegacyCoverCommand {
-  LEGACY_COVER_COMMAND_OPEN = 0,
-  LEGACY_COVER_COMMAND_CLOSE = 1,
-  LEGACY_COVER_COMMAND_STOP = 2,
-};
-
-class CoverCommandRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-  uint32_t get_key() const;
-  optional<LegacyCoverCommand> get_legacy_command() const;
-  optional<float> get_position() const;
-  optional<float> get_tilt() const;
-  bool get_stop() const { return this->stop_; }
-
- protected:
-  uint32_t key_{0};
-  bool has_legacy_command_{false};
-  LegacyCoverCommand legacy_command_{LEGACY_COVER_COMMAND_OPEN};
-  bool has_position_{false};
-  float position_{0.0f};
-  bool has_tilt_{false};
-  float tilt_{0.0f};
-  bool stop_{false};
-};
-#endif
-
-#ifdef USE_FAN
-class FanCommandRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-  uint32_t get_key() const;
-  optional<bool> get_state() const;
-  optional<fan::FanSpeed> get_speed() const;
-  optional<bool> get_oscillating() const;
-
- protected:
-  uint32_t key_{0};
-  bool has_state_{false};
-  bool state_{false};
-  bool has_speed_{false};
-  fan::FanSpeed speed_{fan::FAN_SPEED_LOW};
-  bool has_oscillating_{false};
-  bool oscillating_{false};
-};
-#endif
-
-#ifdef USE_LIGHT
-class LightCommandRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-  uint32_t get_key() const;
-  optional<bool> get_state() const;
-  optional<float> get_brightness() const;
-  optional<float> get_red() const;
-  optional<float> get_green() const;
-  optional<float> get_blue() const;
-  optional<float> get_white() const;
-  optional<float> get_color_temperature() const;
-  optional<uint32_t> get_transition_length() const;
-  optional<uint32_t> get_flash_length() const;
-  optional<std::string> get_effect() const;
-
- protected:
-  uint32_t key_{0};
-  bool has_state_{false};
-  bool state_{false};
-  bool has_brightness_{false};
-  float brightness_{0.0f};
-  bool has_rgb_{false};
-  float red_{0.0f};
-  float green_{0.0f};
-  float blue_{0.0f};
-  bool has_white_{false};
-  float white_{0.0f};
-  bool has_color_temperature_{false};
-  float color_temperature_{0.0f};
-  bool has_transition_length_{false};
-  uint32_t transition_length_{0};
-  bool has_flash_length_{false};
-  uint32_t flash_length_{0};
-  bool has_effect_{false};
-  std::string effect_{};
-};
-#endif
-
-#ifdef USE_SWITCH
-class SwitchCommandRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-  uint32_t get_key() const;
-  bool get_state() const;
-
- protected:
-  uint32_t key_{0};
-  bool state_{false};
-};
-#endif
-
-#ifdef USE_ESP32_CAMERA
-class CameraImageRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool get_single() const;
-  bool get_stream() const;
-  APIMessageType message_type() const override;
-
- protected:
-  bool single_{false};
-  bool stream_{false};
-};
-#endif
-
-#ifdef USE_CLIMATE
-class ClimateCommandRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-  uint32_t get_key() const;
-  optional<climate::ClimateMode> get_mode() const;
-  optional<float> get_target_temperature() const;
-  optional<float> get_target_temperature_low() const;
-  optional<float> get_target_temperature_high() const;
-  optional<bool> get_away() const;
-
- protected:
-  uint32_t key_{0};
-  bool has_mode_{false};
-  climate::ClimateMode mode_{climate::CLIMATE_MODE_OFF};
-  bool has_target_temperature_{false};
-  float target_temperature_{0.0f};
-  bool has_target_temperature_low_{false};
-  float target_temperature_low_{0.0f};
-  bool has_target_temperature_high_{false};
-  float target_temperature_high_{0.0f};
-  bool has_away_{false};
-  bool away_{false};
-};
-#endif
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h
new file mode 100644
index 0000000000..aac91244b6
--- /dev/null
+++ b/esphome/components/api/custom_api_device.h
@@ -0,0 +1,214 @@
+#pragma once
+
+#include <map>
+#include "user_services.h"
+#include "api_server.h"
+
+namespace esphome {
+namespace api {
+
+template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
+ public:
+  CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
+                         void (T::*callback)(Ts...))
+      : UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
+
+ protected:
+  void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); }  // NOLINT
+
+  T *obj_;
+  void (T::*callback_)(Ts...);
+};
+
+class CustomAPIDevice {
+ public:
+  /// Return if a client (such as Home Assistant) is connected to the native API.
+  bool is_connected() const { return global_api_server->is_connected(); }
+
+  /** Register a custom native API service that will show up in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   register_service(&CustomNativeAPI::on_start_washer_cycle, "start_washer_cycle",
+   *                    {"cycle_length"});
+   * }
+   *
+   * void on_start_washer_cycle(int cycle_length) {
+   *   // Start washer cycle.
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @tparam Ts The argument types for the service, automatically deduced from the function arguments.
+   * @param callback The member function to call when the service is triggered.
+   * @param name The name of the service to register.
+   * @param arg_names The name of the arguments for the service, must match the arguments of the function.
+   */
+  template<typename T, typename... Ts>
+  void register_service(void (T::*callback)(Ts...), const std::string &name,
+                        const std::array<std::string, sizeof...(Ts)> &arg_names) {
+    auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);
+    global_api_server->register_user_service(service);
+  }
+
+  /** Register a custom native API service that will show up in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   register_service(&CustomNativeAPI::on_hello_world, "hello_world");
+   * }
+   *
+   * void on_hello_world() {
+   *   // Hello World service called.
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @param callback The member function to call when the service is triggered.
+   * @param name The name of the arguments for the service, must match the arguments of the function.
+   */
+  template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
+    auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);
+    global_api_server->register_user_service(service);
+  }
+
+  /** Subscribe to the state of an entity from Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
+   * }
+   *
+   * void on_state_changed(std::string state) {
+   *   // State of sensor.weather_forecast is `state`
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @param callback The member function to call when the entity state changes.
+   * @param entity_id The entity_id to track.
+   */
+  template<typename T>
+  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id) {
+    auto f = std::bind(callback, (T *) this, std::placeholders::_1);
+    global_api_server->subscribe_home_assistant_state(entity_id, f);
+  }
+
+  /** Subscribe to the state of an entity from Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * void setup() override {
+   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
+   * }
+   *
+   * void on_state_changed(std::string entity_id, std::string state) {
+   *   // State of `entity_id` is `state`
+   * }
+   * ```
+   *
+   * @tparam T The class type creating the service, automatically deduced from the function pointer.
+   * @param callback The member function to call when the entity state changes.
+   * @param entity_id The entity_id to track.
+   */
+  template<typename T>
+  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id) {
+    auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
+    global_api_server->subscribe_home_assistant_state(entity_id, f);
+  }
+
+  /** Call a Home Assistant service from ESPHome.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * call_homeassistant_service("homeassistant.restart");
+   * ```
+   *
+   * @param service_name The service to call.
+   */
+  void call_homeassistant_service(const std::string &service_name) {
+    HomeassistantServiceResponse resp;
+    resp.service = service_name;
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+
+  /** Call a Home Assistant service from ESPHome.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * call_homeassistant_service("light.turn_on", {
+   *   {"entity_id", "light.my_light"},
+   *   {"brightness", "127"},
+   * });
+   * ```
+   *
+   * @param service_name The service to call.
+   * @param data The data for the service call, mapping from string to string.
+   */
+  void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
+    HomeassistantServiceResponse resp;
+    resp.service = service_name;
+    for (auto &it : data) {
+      HomeassistantServiceMap kv;
+      kv.key = it.first;
+      kv.value = it.second;
+      resp.data.push_back(kv);
+    }
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+
+  /** Fire an ESPHome event in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * fire_homeassistant_event("esphome.something_happened");
+   * ```
+   *
+   * @param event_name The event to fire.
+   */
+  void fire_homeassistant_event(const std::string &event_name) {
+    HomeassistantServiceResponse resp;
+    resp.service = event_name;
+    resp.is_event = true;
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+
+  /** Fire an ESPHome event in Home Assistant.
+   *
+   * Usage:
+   *
+   * ```cpp
+   * fire_homeassistant_event("esphome.something_happened", {
+   *   {"my_value", "500"},
+   * });
+   * ```
+   *
+   * @param event_name The event to fire.
+   * @param data The data for the event, mapping from string to string.
+   */
+  void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
+    HomeassistantServiceResponse resp;
+    resp.service = service_name;
+    resp.is_event = true;
+    for (auto &it : data) {
+      HomeassistantServiceMap kv;
+      kv.key = it.first;
+      kv.value = it.second;
+      resp.data.push_back(kv);
+    }
+    global_api_server->send_homeassistant_service_call(resp);
+  }
+};
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h
new file mode 100644
index 0000000000..d68dac3b61
--- /dev/null
+++ b/esphome/components/api/homeassistant_service.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/automation.h"
+#include "api_pb2.h"
+#include "api_server.h"
+
+namespace esphome {
+namespace api {
+
+template<typename... Ts> class TemplatableKeyValuePair {
+ public:
+  template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
+  std::string key;
+  TemplatableStringValue<Ts...> value;
+};
+
+template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
+ public:
+  explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
+
+  TEMPLATABLE_STRING_VALUE(service);
+  template<typename T> void add_data(std::string key, T value) {
+    this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
+  }
+  template<typename T> void add_data_template(std::string key, T value) {
+    this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
+  }
+  template<typename T> void add_variable(std::string key, T value) {
+    this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
+  }
+  void play(Ts... x) override {
+    HomeassistantServiceResponse resp;
+    resp.service = this->service_.value(x...);
+    resp.is_event = this->is_event_;
+    for (auto &it : this->data_) {
+      HomeassistantServiceMap kv;
+      kv.key = it.key;
+      kv.value = it.value.value(x...);
+      resp.data.push_back(kv);
+    }
+    for (auto &it : this->data_template_) {
+      HomeassistantServiceMap kv;
+      kv.key = it.key;
+      kv.value = it.value.value(x...);
+      resp.data_template.push_back(kv);
+    }
+    for (auto &it : this->variables_) {
+      HomeassistantServiceMap kv;
+      kv.key = it.key;
+      kv.value = it.value.value(x...);
+      resp.variables.push_back(kv);
+    }
+    this->parent_->send_homeassistant_service_call(resp);
+  }
+
+ protected:
+  APIServer *parent_;
+  bool is_event_;
+  std::vector<TemplatableKeyValuePair<Ts...>> data_;
+  std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
+  std::vector<TemplatableKeyValuePair<Ts...>> variables_;
+};
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp
index ae63d70448..d4245136ae 100644
--- a/esphome/components/api/list_entities.cpp
+++ b/esphome/components/api/list_entities.cpp
@@ -2,189 +2,54 @@
 #include "esphome/core/util.h"
 #include "esphome/core/log.h"
 #include "esphome/core/application.h"
+#include "api_connection.h"
 
 namespace esphome {
 namespace api {
 
-std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
-  return App.get_name() + component_type + nameable->get_object_id();
-}
-
 #ifdef USE_BINARY_SENSOR
 bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(binary_sensor);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("binary_sensor", binary_sensor));
-  // string device_class = 5;
-  buffer.encode_string(5, binary_sensor->get_device_class());
-  // bool is_status_binary_sensor = 6;
-  buffer.encode_bool(6, binary_sensor->is_status_binary_sensor());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_BINARY_SENSOR_RESPONSE);
+  return this->client_->send_binary_sensor_info(binary_sensor);
 }
 #endif
 #ifdef USE_COVER
-bool ListEntitiesIterator::on_cover(cover::Cover *cover) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(cover);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("cover", cover));
-  auto traits = cover->get_traits();
-
-  // bool assumed_state = 5;
-  buffer.encode_bool(5, traits.get_is_assumed_state());
-  // bool supports_position = 6;
-  buffer.encode_bool(6, traits.get_supports_position());
-  // bool supports_tilt = 7;
-  buffer.encode_bool(7, traits.get_supports_tilt());
-  // string device_class = 8;
-  buffer.encode_string(8, cover->get_device_class());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_COVER_RESPONSE);
-}
+bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); }
 #endif
 #ifdef USE_FAN
-bool ListEntitiesIterator::on_fan(fan::FanState *fan) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(fan);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("fan", fan));
-  // bool supports_oscillation = 5;
-  buffer.encode_bool(5, fan->get_traits().supports_oscillation());
-  // bool supports_speed = 6;
-  buffer.encode_bool(6, fan->get_traits().supports_speed());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_FAN_RESPONSE);
-}
+bool ListEntitiesIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_info(fan); }
 #endif
 #ifdef USE_LIGHT
-bool ListEntitiesIterator::on_light(light::LightState *light) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(light);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("light", light));
-  // bool supports_brightness = 5;
-  auto traits = light->get_traits();
-  buffer.encode_bool(5, traits.get_supports_brightness());
-  // bool supports_rgb = 6;
-  buffer.encode_bool(6, traits.get_supports_rgb());
-  // bool supports_white_value = 7;
-  buffer.encode_bool(7, traits.get_supports_rgb_white_value());
-  // bool supports_color_temperature = 8;
-  buffer.encode_bool(8, traits.get_supports_color_temperature());
-  if (traits.get_supports_color_temperature()) {
-    // float min_mireds = 9;
-    buffer.encode_float(9, traits.get_min_mireds());
-    // float max_mireds = 10;
-    buffer.encode_float(10, traits.get_max_mireds());
-  }
-  // repeated string effects = 11;
-  if (light->supports_effects()) {
-    buffer.encode_string(11, "None");
-    for (auto *effect : light->get_effects()) {
-      buffer.encode_string(11, effect->get_name());
-    }
-  }
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_LIGHT_RESPONSE);
-}
+bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); }
 #endif
 #ifdef USE_SENSOR
-bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(sensor);
-  // string unique_id = 4;
-  std::string unique_id = sensor->unique_id();
-  if (unique_id.empty())
-    unique_id = get_default_unique_id("sensor", sensor);
-  buffer.encode_string(4, unique_id);
-  // string icon = 5;
-  buffer.encode_string(5, sensor->get_icon());
-  // string unit_of_measurement = 6;
-  buffer.encode_string(6, sensor->get_unit_of_measurement());
-  // int32 accuracy_decimals = 7;
-  buffer.encode_int32(7, sensor->get_accuracy_decimals());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SENSOR_RESPONSE);
-}
+bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_info(sensor); }
 #endif
 #ifdef USE_SWITCH
-bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(a_switch);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("switch", a_switch));
-  // string icon = 5;
-  buffer.encode_string(5, a_switch->get_icon());
-  // bool assumed_state = 6;
-  buffer.encode_bool(6, a_switch->assumed_state());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SWITCH_RESPONSE);
-}
+bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); }
 #endif
 #ifdef USE_TEXT_SENSOR
 bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(text_sensor);
-  // string unique_id = 4;
-  std::string unique_id = text_sensor->unique_id();
-  if (unique_id.empty())
-    unique_id = get_default_unique_id("text_sensor", text_sensor);
-  buffer.encode_string(4, unique_id);
-  // string icon = 5;
-  buffer.encode_string(5, text_sensor->get_icon());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_TEXT_SENSOR_RESPONSE);
+  return this->client_->send_text_sensor_info(text_sensor);
 }
 #endif
 
-bool ListEntitiesIterator::on_end() {
-  return this->client_->send_empty_message(APIMessageType::LIST_ENTITIES_DONE_RESPONSE);
-}
+bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
 ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client)
     : ComponentIterator(server), client_(client) {}
 bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
-  auto buffer = this->client_->get_buffer();
-  service->encode_list_service_response(buffer);
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE);
+  auto resp = service->encode_list_service_response();
+  return this->client_->send_list_entities_services_response(resp);
 }
 
 #ifdef USE_ESP32_CAMERA
 bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(camera);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("camera", camera));
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE);
+  return this->client_->send_camera_info(camera);
 }
 #endif
 
 #ifdef USE_CLIMATE
-bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
-  auto buffer = this->client_->get_buffer();
-  buffer.encode_nameable(climate);
-  // string unique_id = 4;
-  buffer.encode_string(4, get_default_unique_id("climate", climate));
-
-  auto traits = climate->get_traits();
-  // bool supports_current_temperature = 5;
-  buffer.encode_bool(5, traits.get_supports_current_temperature());
-  // bool supports_two_point_target_temperature = 6;
-  buffer.encode_bool(6, traits.get_supports_two_point_target_temperature());
-  // repeated ClimateMode supported_modes = 7;
-  for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
-                    climate::CLIMATE_MODE_HEAT}) {
-    if (traits.supports_mode(mode))
-      buffer.encode_uint32(7, mode, true);
-  }
-
-  // float visual_min_temperature = 8;
-  buffer.encode_float(8, traits.get_visual_min_temperature());
-  // float visual_max_temperature = 9;
-  buffer.encode_float(9, traits.get_visual_max_temperature());
-  // float visual_temperature_step = 10;
-  buffer.encode_float(10, traits.get_visual_temperature_step());
-  // bool supports_away = 11;
-  buffer.encode_bool(11, traits.get_supports_away());
-  return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CLIMATE_RESPONSE);
-}
+bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); }
 #endif
 
-APIMessageType ListEntitiesRequest::message_type() const { return APIMessageType::LIST_ENTITIES_REQUEST; }
-
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h
index c1c93e91f8..6b10a72fdf 100644
--- a/esphome/components/api/list_entities.h
+++ b/esphome/components/api/list_entities.h
@@ -2,16 +2,11 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/defines.h"
-#include "api_message.h"
+#include "util.h"
 
 namespace esphome {
 namespace api {
 
-class ListEntitiesRequest : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
 class APIConnection;
 
 class ListEntitiesIterator : public ComponentIterator {
diff --git a/esphome/components/api/api_message.cpp b/esphome/components/api/proto.cpp
similarity index 64%
rename from esphome/components/api/api_message.cpp
rename to esphome/components/api/proto.cpp
index 157ddba2b5..3d2f669f54 100644
--- a/esphome/components/api/api_message.cpp
+++ b/esphome/components/api/proto.cpp
@@ -1,61 +1,59 @@
-#include "api_message.h"
+#include "proto.h"
+#include "util.h"
 #include "esphome/core/log.h"
 
 namespace esphome {
 namespace api {
 
-static const char *TAG = "api.message";
+static const char *TAG = "api.proto";
 
-bool APIMessage::decode_varint(uint32_t field_id, uint32_t value) { return false; }
-bool APIMessage::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { return false; }
-bool APIMessage::decode_32bit(uint32_t field_id, uint32_t value) { return false; }
-void APIMessage::encode(APIBuffer &buffer) {}
-void APIMessage::decode(const uint8_t *buffer, size_t length) {
+void ProtoMessage::decode(const uint8_t *buffer, size_t length) {
   uint32_t i = 0;
   bool error = false;
   while (i < length) {
     uint32_t consumed;
-    auto res = proto_decode_varuint32(&buffer[i], length - i, &consumed);
+    auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
     if (!res.has_value()) {
       ESP_LOGV(TAG, "Invalid field start at %u", i);
       break;
     }
 
-    uint32_t field_type = (*res) & 0b111;
-    uint32_t field_id = (*res) >> 3;
+    uint32_t field_type = (res->as_uint32()) & 0b111;
+    uint32_t field_id = (res->as_uint32()) >> 3;
     i += consumed;
 
     switch (field_type) {
       case 0: {  // VarInt
-        res = proto_decode_varuint32(&buffer[i], length - i, &consumed);
+        res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
         if (!res.has_value()) {
           ESP_LOGV(TAG, "Invalid VarInt at %u", i);
           error = true;
           break;
         }
         if (!this->decode_varint(field_id, *res)) {
-          ESP_LOGV(TAG, "Cannot decode VarInt field %u with value %u!", field_id, *res);
+          ESP_LOGV(TAG, "Cannot decode VarInt field %u with value %u!", field_id, res->as_uint32());
         }
         i += consumed;
         break;
       }
       case 2: {  // Length-delimited
-        res = proto_decode_varuint32(&buffer[i], length - i, &consumed);
+        res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
         if (!res.has_value()) {
           ESP_LOGV(TAG, "Invalid Length Delimited at %u", i);
           error = true;
           break;
         }
+        uint32_t field_length = res->as_uint32();
         i += consumed;
-        if (*res > length - i) {
+        if (field_length > length - i) {
           ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %u", i);
           error = true;
           break;
         }
-        if (!this->decode_length_delimited(field_id, &buffer[i], *res)) {
+        if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) {
           ESP_LOGV(TAG, "Cannot decode Length Delimited field %u!", field_id);
         }
-        i += *res;
+        i += field_length;
         break;
       }
       case 5: {  // 32-bit
@@ -66,7 +64,7 @@ void APIMessage::decode(const uint8_t *buffer, size_t length) {
         }
         uint32_t val = (uint32_t(buffer[i]) << 0) | (uint32_t(buffer[i + 1]) << 8) | (uint32_t(buffer[i + 2]) << 16) |
                        (uint32_t(buffer[i + 3]) << 24);
-        if (!this->decode_32bit(field_id, val)) {
+        if (!this->decode_32bit(field_id, Proto32Bit(val))) {
           ESP_LOGV(TAG, "Cannot decode 32-bit field %u with value %u!", field_id, val);
         }
         i += 4;
@@ -83,5 +81,11 @@ void APIMessage::decode(const uint8_t *buffer, size_t length) {
   }
 }
 
+std::string ProtoMessage::dump() const {
+  std::string out;
+  this->dump_to(out);
+  return out;
+}
+
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h
new file mode 100644
index 0000000000..b0160bf240
--- /dev/null
+++ b/esphome/components/api/proto.h
@@ -0,0 +1,279 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace api {
+
+/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
+class ProtoVarInt {
+ public:
+  ProtoVarInt() : value_(0) {}
+  explicit ProtoVarInt(uint64_t value) : value_(value) {}
+
+  static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
+    if (consumed != nullptr)
+      *consumed = 0;
+
+    if (len == 0)
+      return {};
+
+    uint64_t result = 0;
+    uint8_t bitpos = 0;
+
+    for (uint32_t i = 0; i < len; i++) {
+      uint8_t val = buffer[i];
+      result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
+      bitpos += 7;
+      if ((val & 0x80) == 0) {
+        if (consumed != nullptr)
+          *consumed = i + 1;
+        return ProtoVarInt(result);
+      }
+    }
+
+    return {};
+  }
+
+  uint32_t as_uint32() const { return this->value_; }
+  uint64_t as_uint64() const { return this->value_; }
+  bool as_bool() const { return this->value_; }
+  template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); }
+  int32_t as_int32() const {
+    // Not ZigZag encoded
+    return static_cast<int32_t>(this->as_int64());
+  }
+  int64_t as_int64() const {
+    // Not ZigZag encoded
+    return static_cast<int64_t>(this->value_);
+  }
+  int32_t as_sint32() const {
+    // with ZigZag encoding
+    if (this->value_ & 1)
+      return static_cast<int32_t>(~(this->value_ >> 1));
+    else
+      return static_cast<int32_t>(this->value_ >> 1);
+  }
+  int64_t as_sint64() const {
+    // with ZigZag encoding
+    if (this->value_ & 1)
+      return static_cast<int64_t>(~(this->value_ >> 1));
+    else
+      return static_cast<int64_t>(this->value_ >> 1);
+  }
+  void encode(std::vector<uint8_t> &out) {
+    uint32_t val = this->value_;
+    if (val <= 0x7F) {
+      out.push_back(val);
+      return;
+    }
+    while (val) {
+      uint8_t temp = val & 0x7F;
+      val >>= 7;
+      if (val) {
+        out.push_back(temp | 0x80);
+      } else {
+        out.push_back(temp);
+      }
+    }
+  }
+
+ protected:
+  uint64_t value_;
+};
+
+class ProtoLengthDelimited {
+ public:
+  explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
+  std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
+  template<class C> C as_message() const {
+    auto msg = C();
+    msg.decode(this->value_, this->length_);
+    return msg;
+  }
+
+ protected:
+  const uint8_t *const value_;
+  const size_t length_;
+};
+
+class Proto32Bit {
+ public:
+  explicit Proto32Bit(uint32_t value) : value_(value) {}
+  uint32_t as_fixed32() const { return this->value_; }
+  int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
+  float as_float() const {
+    union {
+      uint32_t raw;
+      float value;
+    } s{};
+    s.raw = this->value_;
+    return s.value;
+  }
+
+ protected:
+  const uint32_t value_;
+};
+
+class Proto64Bit {
+ public:
+  explicit Proto64Bit(uint64_t value) : value_(value) {}
+  uint64_t as_fixed64() const { return this->value_; }
+  int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); }
+  double as_double() const {
+    union {
+      uint64_t raw;
+      double value;
+    } s{};
+    s.raw = this->value_;
+    return s.value;
+  }
+
+ protected:
+  const uint64_t value_;
+};
+
+class ProtoWriteBuffer {
+ public:
+  ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
+  void write(uint8_t value) { this->buffer_->push_back(value); }
+  void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
+  void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
+  void encode_field_raw(uint32_t field_id, uint32_t type) {
+    uint32_t val = (field_id << 3) | (type & 0b111);
+    this->encode_varint_raw(val);
+  }
+  void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
+    if (len == 0 && !force)
+      return;
+
+    this->encode_field_raw(field_id, 2);
+    this->encode_varint_raw(len);
+    auto *data = reinterpret_cast<const uint8_t *>(string);
+    for (size_t i = 0; i < len; i++)
+      this->write(data[i]);
+  }
+  void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
+    this->encode_string(field_id, value.data(), value.size());
+  }
+  void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
+    this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
+  }
+  void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
+    if (value == 0 && !force)
+      return;
+    this->encode_field_raw(field_id, 0);
+    this->encode_varint_raw(value);
+  }
+  void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
+    if (value == 0 && !force)
+      return;
+    this->encode_field_raw(field_id, 0);
+    this->encode_varint_raw(ProtoVarInt(value));
+  }
+  void encode_bool(uint32_t field_id, bool value, bool force = false) {
+    if (!value && !force)
+      return;
+    this->encode_field_raw(field_id, 0);
+    this->write(0x01);
+  }
+  void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
+    if (value == 0 && !force)
+      return;
+
+    this->encode_field_raw(field_id, 5);
+    this->write((value >> 0) & 0xFF);
+    this->write((value >> 8) & 0xFF);
+    this->write((value >> 16) & 0xFF);
+    this->write((value >> 24) & 0xFF);
+  }
+  template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
+    this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
+  }
+  void encode_float(uint32_t field_id, float value, bool force = false) {
+    if (value == 0.0f && !force)
+      return;
+
+    union {
+      float value;
+      uint32_t raw;
+    } val{};
+    val.value = value;
+    this->encode_fixed32(field_id, val.raw);
+  }
+  void encode_int32(uint32_t field_id, int32_t value, bool force = false) {
+    if (value < 0) {
+      // negative int32 is always 10 byte long
+      this->encode_int64(field_id, value, force);
+      return;
+    }
+    this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
+  }
+  void encode_int64(uint32_t field_id, int64_t value, bool force = false) {
+    this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
+  }
+  void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
+    uint32_t uvalue;
+    if (value < 0)
+      uvalue = ~(value << 1);
+    else
+      uvalue = value << 1;
+    this->encode_uint32(field_id, uvalue, force);
+  }
+  template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
+    this->encode_field_raw(field_id, 2);
+    size_t begin = this->buffer_->size();
+
+    value.encode(*this);
+
+    const uint32_t nested_length = this->buffer_->size() - begin;
+    // add size varint
+    std::vector<uint8_t> var;
+    ProtoVarInt(nested_length).encode(var);
+    this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end());
+  }
+  std::vector<uint8_t> *get_buffer() const { return buffer_; }
+
+ protected:
+  std::vector<uint8_t> *buffer_;
+};
+
+class ProtoMessage {
+ public:
+  virtual void encode(ProtoWriteBuffer buffer) const = 0;
+  void decode(const uint8_t *buffer, size_t length);
+  std::string dump() const;
+  virtual void dump_to(std::string &out) const = 0;
+
+ protected:
+  virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
+  virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
+  virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
+  virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; }
+};
+
+template<typename T> const char *proto_enum_to_string(T value);
+
+class ProtoService {
+ public:
+ protected:
+  virtual bool is_authenticated() = 0;
+  virtual bool is_connection_setup() = 0;
+  virtual void on_fatal_error() = 0;
+  virtual void on_unauthenticated_access() = 0;
+  virtual void on_no_setup_connection() = 0;
+  virtual ProtoWriteBuffer create_buffer() = 0;
+  virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0;
+  virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
+  virtual void set_nodelay(bool nodelay) = 0;
+
+  template<class C> bool send_message_(const C &msg, uint32_t message_type) {
+    auto buffer = this->create_buffer();
+    msg.encode(buffer);
+    return this->send_buffer(buffer, message_type);
+  }
+};
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/service_call_message.cpp b/esphome/components/api/service_call_message.cpp
deleted file mode 100644
index f4c5741c94..0000000000
--- a/esphome/components/api/service_call_message.cpp
+++ /dev/null
@@ -1,49 +0,0 @@
-#include "service_call_message.h"
-#include "esphome/core/log.h"
-
-namespace esphome {
-namespace api {
-
-APIMessageType SubscribeServiceCallsRequest::message_type() const {
-  return APIMessageType::SUBSCRIBE_SERVICE_CALLS_REQUEST;
-}
-
-APIMessageType ServiceCallResponse::message_type() const { return APIMessageType::SERVICE_CALL_RESPONSE; }
-void ServiceCallResponse::encode(APIBuffer &buffer) {
-  // string service = 1;
-  buffer.encode_string(1, this->service_);
-  // map<string, string> data = 2;
-  for (auto &it : this->data_) {
-    auto nested = buffer.begin_nested(2);
-    buffer.encode_string(1, it.key);
-    buffer.encode_string(2, it.value);
-    buffer.end_nested(nested);
-  }
-  // map<string, string> data_template = 3;
-  for (auto &it : this->data_template_) {
-    auto nested = buffer.begin_nested(3);
-    buffer.encode_string(1, it.key);
-    buffer.encode_string(2, it.value);
-    buffer.end_nested(nested);
-  }
-  // map<string, string> variables = 4;
-  for (auto &it : this->variables_) {
-    auto nested = buffer.begin_nested(4);
-    buffer.encode_string(1, it.key);
-    buffer.encode_string(2, it.value());
-    buffer.end_nested(nested);
-  }
-}
-void ServiceCallResponse::set_service(const std::string &service) { this->service_ = service; }
-void ServiceCallResponse::set_data(const std::vector<KeyValuePair> &data) { this->data_ = data; }
-void ServiceCallResponse::set_data_template(const std::vector<KeyValuePair> &data_template) {
-  this->data_template_ = data_template;
-}
-void ServiceCallResponse::set_variables(const std::vector<TemplatableKeyValuePair> &variables) {
-  this->variables_ = variables;
-}
-
-KeyValuePair::KeyValuePair(const std::string &key, const std::string &value) : key(key), value(value) {}
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/service_call_message.h b/esphome/components/api/service_call_message.h
deleted file mode 100644
index bb9d17a110..0000000000
--- a/esphome/components/api/service_call_message.h
+++ /dev/null
@@ -1,53 +0,0 @@
-#pragma once
-
-#include "esphome/core/helpers.h"
-#include "esphome/core/automation.h"
-#include "api_message.h"
-
-namespace esphome {
-namespace api {
-
-class SubscribeServiceCallsRequest : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
-class KeyValuePair {
- public:
-  KeyValuePair(const std::string &key, const std::string &value);
-
-  std::string key;
-  std::string value;
-};
-
-class TemplatableKeyValuePair {
- public:
-  template<typename T> TemplatableKeyValuePair(std::string key, T func);
-
-  std::string key;
-  std::function<std::string()> value;
-};
-template<typename T> TemplatableKeyValuePair::TemplatableKeyValuePair(std::string key, T func) : key(key) {
-  this->value = [func]() -> std::string { return to_string(func()); };
-}
-
-class ServiceCallResponse : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-
-  void encode(APIBuffer &buffer) override;
-
-  void set_service(const std::string &service);
-  void set_data(const std::vector<KeyValuePair> &data);
-  void set_data_template(const std::vector<KeyValuePair> &data_template);
-  void set_variables(const std::vector<TemplatableKeyValuePair> &variables);
-
- protected:
-  std::string service_;
-  std::vector<KeyValuePair> data_;
-  std::vector<KeyValuePair> data_template_;
-  std::vector<TemplatableKeyValuePair> variables_;
-};
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/subscribe_logs.cpp b/esphome/components/api/subscribe_logs.cpp
deleted file mode 100644
index f7f5be9fad..0000000000
--- a/esphome/components/api/subscribe_logs.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-#include "subscribe_logs.h"
-#include "esphome/core/log.h"
-
-namespace esphome {
-namespace api {
-
-APIMessageType SubscribeLogsRequest::message_type() const { return APIMessageType::SUBSCRIBE_LOGS_REQUEST; }
-bool SubscribeLogsRequest::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:  // LogLevel level = 1;
-      this->level_ = value;
-      return true;
-    case 2:  // bool dump_config = 2;
-      this->dump_config_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-uint32_t SubscribeLogsRequest::get_level() const { return this->level_; }
-void SubscribeLogsRequest::set_level(uint32_t level) { this->level_ = level; }
-bool SubscribeLogsRequest::get_dump_config() const { return this->dump_config_; }
-void SubscribeLogsRequest::set_dump_config(bool dump_config) { this->dump_config_ = dump_config; }
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/subscribe_logs.h b/esphome/components/api/subscribe_logs.h
deleted file mode 100644
index f9b0db75b1..0000000000
--- a/esphome/components/api/subscribe_logs.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include "esphome/core/component.h"
-#include "api_message.h"
-
-namespace esphome {
-namespace api {
-
-class SubscribeLogsRequest : public APIMessage {
- public:
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-  uint32_t get_level() const;
-  void set_level(uint32_t level);
-  bool get_dump_config() const;
-  void set_dump_config(bool dump_config);
-
- protected:
-  uint32_t level_{6};
-  bool dump_config_{false};
-};
-
-}  // namespace api
-}  // namespace esphome
diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp
index 8f312d5421..50d674bee2 100644
--- a/esphome/components/api/subscribe_state.cpp
+++ b/esphome/components/api/subscribe_state.cpp
@@ -1,4 +1,5 @@
 #include "subscribe_state.h"
+#include "api_connection.h"
 #include "esphome/core/log.h"
 
 namespace esphome {
@@ -48,30 +49,5 @@ bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->
 InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
     : ComponentIterator(server), client_(client) {}
 
-APIMessageType SubscribeStatesRequest::message_type() const { return APIMessageType::SUBSCRIBE_STATES_REQUEST; }
-
-bool HomeAssistantStateResponse::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 1:
-      // string entity_id = 1;
-      this->entity_id_ = as_string(value, len);
-      return true;
-    case 2:
-      // string state = 2;
-      this->state_ = as_string(value, len);
-      return true;
-    default:
-      return false;
-  }
-}
-APIMessageType HomeAssistantStateResponse::message_type() const {
-  return APIMessageType::HOME_ASSISTANT_STATE_RESPONSE;
-}
-const std::string &HomeAssistantStateResponse::get_entity_id() const { return this->entity_id_; }
-const std::string &HomeAssistantStateResponse::get_state() const { return this->state_; }
-APIMessageType SubscribeHomeAssistantStatesRequest::message_type() const {
-  return APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST;
-}
-
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h
index 43b919f2e8..51b9c695e4 100644
--- a/esphome/components/api/subscribe_state.h
+++ b/esphome/components/api/subscribe_state.h
@@ -4,16 +4,10 @@
 #include "esphome/core/controller.h"
 #include "esphome/core/defines.h"
 #include "util.h"
-#include "api_message.h"
 
 namespace esphome {
 namespace api {
 
-class SubscribeStatesRequest : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
 class APIConnection;
 
 class InitialStateIterator : public ComponentIterator {
@@ -47,23 +41,6 @@ class InitialStateIterator : public ComponentIterator {
   APIConnection *client_;
 };
 
-class SubscribeHomeAssistantStatesRequest : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-};
-
-class HomeAssistantStateResponse : public APIMessage {
- public:
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  APIMessageType message_type() const override;
-  const std::string &get_entity_id() const;
-  const std::string &get_state() const;
-
- protected:
-  std::string entity_id_;
-  std::string state_;
-};
-
 }  // namespace api
 }  // namespace esphome
 
diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp
index e1dd8932f8..0667d26ff6 100644
--- a/esphome/components/api/user_services.cpp
+++ b/esphome/components/api/user_services.cpp
@@ -4,71 +4,35 @@
 namespace esphome {
 namespace api {
 
-template<> bool ExecuteServiceArgument::get_value<bool>() { return this->value_bool_; }
-template<> int ExecuteServiceArgument::get_value<int>() { return this->value_int_; }
-template<> float ExecuteServiceArgument::get_value<float>() { return this->value_float_; }
-template<> std::string ExecuteServiceArgument::get_value<std::string>() { return this->value_string_; }
-
-APIMessageType ExecuteServiceArgument::message_type() const { return APIMessageType::EXECUTE_SERVICE_REQUEST; }
-bool ExecuteServiceArgument::decode_varint(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:  // bool bool_ = 1;
-      this->value_bool_ = value;
-      return true;
-    case 2:  // int32 int_ = 2;
-      this->value_int_ = value;
-      return true;
-    default:
-      return false;
-  }
+template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
+template<> int get_execute_arg_value<int>(const ExecuteServiceArgument &arg) {
+  if (arg.legacy_int != 0)
+    return arg.legacy_int;
+  return arg.int_;
 }
-bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 3:  // float float_ = 3;
-      this->value_float_ = as_float(value);
-      return true;
-    default:
-      return false;
-  }
+template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
+template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
+template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
+  return arg.bool_array;
 }
-bool ExecuteServiceArgument::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 4:  // string string_ = 4;
-      this->value_string_ = as_string(value, len);
-      return true;
-    default:
-      return false;
-  }
+template<> std::vector<int> get_execute_arg_value<std::vector<int>>(const ExecuteServiceArgument &arg) {
+  return arg.int_array;
+}
+template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
+  return arg.float_array;
+}
+template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
+  return arg.string_array;
 }
 
-bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:  // fixed32 key = 1;
-      this->key_ = value;
-      return true;
-    default:
-      return false;
-  }
-}
-bool ExecuteServiceRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
-  switch (field_id) {
-    case 2: {  // repeated ExecuteServiceArgument args = 2;
-      ExecuteServiceArgument arg;
-      arg.decode(value, len);
-      this->args_.push_back(arg);
-      return true;
-    }
-    default:
-      return false;
-  }
-}
-APIMessageType ExecuteServiceRequest::message_type() const { return APIMessageType::EXECUTE_SERVICE_REQUEST; }
-const std::vector<ExecuteServiceArgument> &ExecuteServiceRequest::get_args() const { return this->args_; }
-uint32_t ExecuteServiceRequest::get_key() const { return this->key_; }
-
-ServiceTypeArgument::ServiceTypeArgument(const std::string &name, ServiceArgType type) : name_(name), type_(type) {}
-const std::string &ServiceTypeArgument::get_name() const { return this->name_; }
-ServiceArgType ServiceTypeArgument::get_type() const { return this->type_; }
+template<> EnumServiceArgType to_service_arg_type<bool>() { return SERVICE_ARG_TYPE_BOOL; }
+template<> EnumServiceArgType to_service_arg_type<int>() { return SERVICE_ARG_TYPE_INT; }
+template<> EnumServiceArgType to_service_arg_type<float>() { return SERVICE_ARG_TYPE_FLOAT; }
+template<> EnumServiceArgType to_service_arg_type<std::string>() { return SERVICE_ARG_TYPE_STRING; }
+template<> EnumServiceArgType to_service_arg_type<std::vector<bool>>() { return SERVICE_ARG_TYPE_BOOL_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type<std::vector<int>>() { return SERVICE_ARG_TYPE_INT_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type<std::vector<float>>() { return SERVICE_ARG_TYPE_FLOAT_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type<std::vector<std::string>>() { return SERVICE_ARG_TYPE_STRING_ARRAY; }
 
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h
index 92e7a2ca0e..3b99d426a9 100644
--- a/esphome/components/api/user_services.h
+++ b/esphome/components/api/user_services.h
@@ -2,124 +2,71 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/automation.h"
-#include "api_message.h"
+#include "api_pb2.h"
 
 namespace esphome {
 namespace api {
 
-enum ServiceArgType {
-  SERVICE_ARG_TYPE_BOOL = 0,
-  SERVICE_ARG_TYPE_INT = 1,
-  SERVICE_ARG_TYPE_FLOAT = 2,
-  SERVICE_ARG_TYPE_STRING = 3,
-};
-
-class ServiceTypeArgument {
- public:
-  ServiceTypeArgument(const std::string &name, ServiceArgType type);
-  const std::string &get_name() const;
-  ServiceArgType get_type() const;
-
- protected:
-  std::string name_;
-  ServiceArgType type_;
-};
-
-class ExecuteServiceArgument : public APIMessage {
- public:
-  APIMessageType message_type() const override;
-  template<typename T> T get_value();
-
-  bool decode_varint(uint32_t field_id, uint32_t value) override;
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-
- protected:
-  bool value_bool_{false};
-  int value_int_{0};
-  float value_float_{0.0f};
-  std::string value_string_{};
-};
-
-class ExecuteServiceRequest : public APIMessage {
- public:
-  bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  APIMessageType message_type() const override;
-
-  uint32_t get_key() const;
-  const std::vector<ExecuteServiceArgument> &get_args() const;
-
- protected:
-  uint32_t key_;
-  std::vector<ExecuteServiceArgument> args_;
-};
-
 class UserServiceDescriptor {
  public:
-  virtual void encode_list_service_response(APIBuffer &buffer) = 0;
+  virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
 
   virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
 };
 
-template<typename... Ts> class UserService : public UserServiceDescriptor, public Trigger<Ts...> {
+template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
+
+template<typename T> EnumServiceArgType to_service_arg_type();
+
+template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
  public:
-  UserService(const std::string &name, const std::array<ServiceTypeArgument, sizeof...(Ts)> &args);
+  UserServiceBase(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
+      : name_(name), arg_names_(arg_names) {
+    this->key_ = fnv1_hash(this->name_);
+  }
 
-  void encode_list_service_response(APIBuffer &buffer) override;
+  ListEntitiesServicesResponse encode_list_service_response() override {
+    ListEntitiesServicesResponse msg;
+    msg.name = this->name_;
+    msg.key = this->key_;
+    std::array<EnumServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
+    for (int i = 0; i < sizeof...(Ts); i++) {
+      ListEntitiesServicesArgument arg;
+      arg.type = arg_types[i];
+      arg.name = this->arg_names_[i];
+      msg.args.push_back(arg);
+    }
+    return msg;
+  }
 
-  bool execute_service(const ExecuteServiceRequest &req) override;
+  bool execute_service(const ExecuteServiceRequest &req) override {
+    if (req.key != this->key_)
+      return false;
+    if (req.args.size() != this->arg_names_.size())
+      return false;
+    this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
+    return true;
+  }
 
  protected:
-  template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>);
+  virtual void execute(Ts... x) = 0;
+  template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) {
+    this->execute((get_execute_arg_value<Ts>(args[S]))...);
+  }
 
   std::string name_;
   uint32_t key_{0};
-  std::array<ServiceTypeArgument, sizeof...(Ts)> args_;
+  std::array<std::string, sizeof...(Ts)> arg_names_;
 };
 
-template<typename... Ts>
-template<int... S>
-void UserService<Ts...>::execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) {
-  this->trigger((args[S].get_value<Ts>())...);
-}
-template<typename... Ts> void UserService<Ts...>::encode_list_service_response(APIBuffer &buffer) {
-  // string name = 1;
-  buffer.encode_string(1, this->name_);
-  // fixed32 key = 2;
-  buffer.encode_fixed32(2, this->key_);
+template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
+ public:
+  UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
+      : UserServiceBase<Ts...>(name, arg_names) {}
 
-  // repeated ListServicesArgument args = 3;
-  for (auto &arg : this->args_) {
-    auto nested = buffer.begin_nested(3);
-    // string name = 1;
-    buffer.encode_string(1, arg.get_name());
-    // Type type = 2;
-    buffer.encode_int32(2, arg.get_type());
-    buffer.end_nested(nested);
-  }
-}
-template<typename... Ts> bool UserService<Ts...>::execute_service(const ExecuteServiceRequest &req) {
-  if (req.get_key() != this->key_)
-    return false;
-
-  if (req.get_args().size() != this->args_.size()) {
-    return false;
-  }
-
-  this->execute_(req.get_args(), typename gens<sizeof...(Ts)>::type());
-  return true;
-}
-template<typename... Ts>
-UserService<Ts...>::UserService(const std::string &name, const std::array<ServiceTypeArgument, sizeof...(Ts)> &args)
-    : name_(name), args_(args) {
-  this->key_ = fnv1_hash(this->name_);
-}
-
-template<> bool ExecuteServiceArgument::get_value<bool>();
-template<> int ExecuteServiceArgument::get_value<int>();
-template<> float ExecuteServiceArgument::get_value<float>();
-template<> std::string ExecuteServiceArgument::get_value<std::string>();
+ protected:
+  void execute(Ts... x) override { this->trigger(x...); }  // NOLINT
+};
 
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp
index 9b8f91ea7d..f929db5d6a 100644
--- a/esphome/components/api/util.cpp
+++ b/esphome/components/api/util.cpp
@@ -7,166 +7,6 @@
 namespace esphome {
 namespace api {
 
-APIBuffer::APIBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
-size_t APIBuffer::get_length() const { return this->buffer_->size(); }
-void APIBuffer::write(uint8_t value) { this->buffer_->push_back(value); }
-void APIBuffer::encode_uint32(uint32_t field, uint32_t value, bool force) {
-  if (value == 0 && !force)
-    return;
-
-  this->encode_field_raw(field, 0);
-  this->encode_varint_raw(value);
-}
-void APIBuffer::encode_int32(uint32_t field, int32_t value, bool force) {
-  this->encode_uint32(field, static_cast<uint32_t>(value), force);
-}
-void APIBuffer::encode_bool(uint32_t field, bool value, bool force) {
-  if (!value && !force)
-    return;
-
-  this->encode_field_raw(field, 0);
-  this->write(0x01);
-}
-void APIBuffer::encode_string(uint32_t field, const std::string &value) {
-  this->encode_string(field, value.data(), value.size());
-}
-void APIBuffer::encode_bytes(uint32_t field, const uint8_t *data, size_t len) {
-  this->encode_string(field, reinterpret_cast<const char *>(data), len);
-}
-void APIBuffer::encode_string(uint32_t field, const char *string, size_t len) {
-  if (len == 0)
-    return;
-
-  this->encode_field_raw(field, 2);
-  this->encode_varint_raw(len);
-  const uint8_t *data = reinterpret_cast<const uint8_t *>(string);
-  for (size_t i = 0; i < len; i++) {
-    this->write(data[i]);
-  }
-}
-void APIBuffer::encode_fixed32(uint32_t field, uint32_t value, bool force) {
-  if (value == 0 && !force)
-    return;
-
-  this->encode_field_raw(field, 5);
-  this->write((value >> 0) & 0xFF);
-  this->write((value >> 8) & 0xFF);
-  this->write((value >> 16) & 0xFF);
-  this->write((value >> 24) & 0xFF);
-}
-void APIBuffer::encode_float(uint32_t field, float value, bool force) {
-  if (value == 0.0f && !force)
-    return;
-
-  union {
-    float value_f;
-    uint32_t value_raw;
-  } val;
-  val.value_f = value;
-  this->encode_fixed32(field, val.value_raw);
-}
-void APIBuffer::encode_field_raw(uint32_t field, uint32_t type) {
-  uint32_t val = (field << 3) | (type & 0b111);
-  this->encode_varint_raw(val);
-}
-void APIBuffer::encode_varint_raw(uint32_t value) {
-  if (value <= 0x7F) {
-    this->write(value);
-    return;
-  }
-
-  while (value) {
-    uint8_t temp = value & 0x7F;
-    value >>= 7;
-    if (value) {
-      this->write(temp | 0x80);
-    } else {
-      this->write(temp);
-    }
-  }
-}
-void APIBuffer::encode_sint32(uint32_t field, int32_t value, bool force) {
-  if (value < 0)
-    this->encode_uint32(field, ~(uint32_t(value) << 1), force);
-  else
-    this->encode_uint32(field, uint32_t(value) << 1, force);
-}
-void APIBuffer::encode_nameable(Nameable *nameable) {
-  // string object_id = 1;
-  this->encode_string(1, nameable->get_object_id());
-  // fixed32 key = 2;
-  this->encode_fixed32(2, nameable->get_object_id_hash());
-  // string name = 3;
-  this->encode_string(3, nameable->get_name());
-}
-size_t APIBuffer::begin_nested(uint32_t field) {
-  this->encode_field_raw(field, 2);
-  return this->buffer_->size();
-}
-void APIBuffer::end_nested(size_t begin_index) {
-  const uint32_t nested_length = this->buffer_->size() - begin_index;
-  // add varint
-  std::vector<uint8_t> var;
-  uint32_t val = nested_length;
-  if (val <= 0x7F) {
-    var.push_back(val);
-  } else {
-    while (val) {
-      uint8_t temp = val & 0x7F;
-      val >>= 7;
-      if (val) {
-        var.push_back(temp | 0x80);
-      } else {
-        var.push_back(temp);
-      }
-    }
-  }
-  this->buffer_->insert(this->buffer_->begin() + begin_index, var.begin(), var.end());
-}
-
-optional<uint32_t> proto_decode_varuint32(const uint8_t *buf, size_t len, uint32_t *consumed) {
-  if (len == 0)
-    return {};
-
-  uint32_t result = 0;
-  uint8_t bitpos = 0;
-
-  for (uint32_t i = 0; i < len; i++) {
-    uint8_t val = buf[i];
-    result |= uint32_t(val & 0x7F) << bitpos;
-    bitpos += 7;
-    if ((val & 0x80) == 0) {
-      if (consumed != nullptr) {
-        *consumed = i + 1;
-      }
-      return result;
-    }
-  }
-
-  return {};
-}
-
-std::string as_string(const uint8_t *value, size_t len) {
-  return std::string(reinterpret_cast<const char *>(value), len);
-}
-
-int32_t as_sint32(uint32_t val) {
-  if (val & 1)
-    return uint32_t(~(val >> 1));
-  else
-    return uint32_t(val >> 1);
-}
-
-float as_float(uint32_t val) {
-  static_assert(sizeof(uint32_t) == sizeof(float), "float must be 32bit long");
-  union {
-    uint32_t raw;
-    float value;
-  } x;
-  x.raw = val;
-  return x.value;
-}
-
 ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {}
 void ComponentIterator::begin() {
   this->state_ = IteratorState::BEGIN;
diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h
index 6478dec07a..5a29a48cbe 100644
--- a/esphome/components/api/util.h
+++ b/esphome/components/api/util.h
@@ -10,40 +10,6 @@
 namespace esphome {
 namespace api {
 
-class APIBuffer {
- public:
-  APIBuffer(std::vector<uint8_t> *buffer);
-
-  size_t get_length() const;
-  void write(uint8_t value);
-
-  void encode_int32(uint32_t field, int32_t value, bool force = false);
-  void encode_uint32(uint32_t field, uint32_t value, bool force = false);
-  void encode_sint32(uint32_t field, int32_t value, bool force = false);
-  void encode_bool(uint32_t field, bool value, bool force = false);
-  void encode_string(uint32_t field, const std::string &value);
-  void encode_string(uint32_t field, const char *string, size_t len);
-  void encode_bytes(uint32_t field, const uint8_t *data, size_t len);
-  void encode_fixed32(uint32_t field, uint32_t value, bool force = false);
-  void encode_float(uint32_t field, float value, bool force = false);
-  void encode_nameable(Nameable *nameable);
-
-  size_t begin_nested(uint32_t field);
-  void end_nested(size_t begin_index);
-
-  void encode_field_raw(uint32_t field, uint32_t type);
-  void encode_varint_raw(uint32_t value);
-
- protected:
-  std::vector<uint8_t> *buffer_;
-};
-
-optional<uint32_t> proto_decode_varuint32(const uint8_t *buf, size_t len, uint32_t *consumed = nullptr);
-
-std::string as_string(const uint8_t *value, size_t len);
-int32_t as_sint32(uint32_t val);
-float as_float(uint32_t val);
-
 class APIServer;
 class UserServiceDescriptor;
 
diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py
new file mode 100644
index 0000000000..de25060623
--- /dev/null
+++ b/esphome/components/as3935/__init__.py
@@ -0,0 +1,46 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \
+    CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \
+    CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAPACITANCE
+from esphome.core import coroutine
+
+AUTO_LOAD = ['sensor', 'binary_sensor']
+MULTI_CONF = True
+
+CONF_AS3935_ID = 'as3935_id'
+
+as3935_ns = cg.esphome_ns.namespace('as3935')
+AS3935 = as3935_ns.class_('AS3935Component', cg.Component)
+
+CONF_IRQ_PIN = 'irq_pin'
+AS3935_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(AS3935),
+    cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
+
+    cv.Optional(CONF_INDOOR, default=True): cv.boolean,
+    cv.Optional(CONF_NOISE_LEVEL, default=2): cv.int_range(min=1, max=7),
+    cv.Optional(CONF_WATCHDOG_THRESHOLD, default=2): cv.int_range(min=1, max=10),
+    cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11),
+    cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of(1, 5, 9, 16, int=True),
+    cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean,
+    cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 22, 64, 128, int=True),
+    cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15),
+})
+
+
+@coroutine
+def setup_as3935(var, config):
+    yield cg.register_component(var, config)
+
+    irq_pin = yield cg.gpio_pin_expression(config[CONF_IRQ_PIN])
+    cg.add(var.set_irq_pin(irq_pin))
+    cg.add(var.set_indoor(config[CONF_INDOOR]))
+    cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL]))
+    cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD]))
+    cg.add(var.set_spike_rejection(config[CONF_SPIKE_REJECTION]))
+    cg.add(var.set_lightning_threshold(config[CONF_LIGHTNING_THRESHOLD]))
+    cg.add(var.set_mask_disturber(config[CONF_MASK_DISTURBER]))
+    cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))
+    cg.add(var.set_capacitance(config[CONF_CAPACITANCE]))
diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp
new file mode 100644
index 0000000000..f8272e6036
--- /dev/null
+++ b/esphome/components/as3935/as3935.cpp
@@ -0,0 +1,223 @@
+#include "as3935.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace as3935 {
+
+static const char *TAG = "as3935";
+
+void AS3935Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up AS3935...");
+
+  this->irq_pin_->setup();
+  LOG_PIN("  IRQ Pin: ", this->irq_pin_);
+
+  // Write properties to sensor
+  this->write_indoor(this->indoor_);
+  this->write_noise_level(this->noise_level_);
+  this->write_watchdog_threshold(this->watchdog_threshold_);
+  this->write_spike_rejection(this->spike_rejection_);
+  this->write_lightning_threshold(this->lightning_threshold_);
+  this->write_mask_disturber(this->mask_disturber_);
+  this->write_div_ratio(this->div_ratio_);
+  this->write_capacitance(this->capacitance_);
+}
+
+void AS3935Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "AS3935:");
+  LOG_PIN("  Interrupt Pin: ", this->irq_pin_);
+}
+
+float AS3935Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void AS3935Component::loop() {
+  if (!this->irq_pin_->digital_read())
+    return;
+
+  uint8_t int_value = this->read_interrupt_register_();
+  if (int_value == NOISE_INT) {
+    ESP_LOGI(TAG, "Noise was detected - try increasing the noise level value!");
+  } else if (int_value == DISTURBER_INT) {
+    ESP_LOGI(TAG, "Disturber was detected - try increasing the spike rejection value!");
+  } else if (int_value == LIGHTNING_INT) {
+    ESP_LOGI(TAG, "Lightning has been detected!");
+    if (this->thunder_alert_binary_sensor_ != nullptr)
+      this->thunder_alert_binary_sensor_->publish_state(true);
+    uint8_t distance = this->get_distance_to_storm_();
+    if (this->distance_sensor_ != nullptr)
+      this->distance_sensor_->publish_state(distance);
+    uint32_t energy = this->get_lightning_energy_();
+    if (this->energy_sensor_ != nullptr)
+      this->energy_sensor_->publish_state(energy);
+  }
+  this->thunder_alert_binary_sensor_->publish_state(false);
+}
+
+void AS3935Component::write_indoor(bool indoor) {
+  ESP_LOGV(TAG, "Setting indoor to %d", indoor);
+  if (indoor)
+    this->write_register(AFE_GAIN, GAIN_MASK, INDOOR, 1);
+  else
+    this->write_register(AFE_GAIN, GAIN_MASK, OUTDOOR, 1);
+}
+// REG0x01, bits[3:0], manufacturer default: 0010 (2).
+// This setting determines the threshold for events that trigger the
+// IRQ Pin.
+void AS3935Component::write_watchdog_threshold(uint8_t watchdog_threshold) {
+  ESP_LOGV(TAG, "Setting watchdog sensitivity to %d", watchdog_threshold);
+  if ((watchdog_threshold < 1) || (watchdog_threshold > 10))  // 10 is the max sensitivity setting
+    return;
+  this->write_register(THRESHOLD, THRESH_MASK, watchdog_threshold, 0);
+}
+
+// REG0x01, bits [6:4], manufacturer default: 010 (2).
+// The noise floor level is compared to a known reference voltage. If this
+// level is exceeded the chip will issue an interrupt to the IRQ pin,
+// broadcasting that it can not operate properly due to noise (INT_NH).
+// Check datasheet for specific noise level tolerances when setting this register.
+void AS3935Component::write_noise_level(uint8_t noise_level) {
+  ESP_LOGV(TAG, "Setting noise level to %d", noise_level);
+  if ((noise_level < 1) || (noise_level > 7))
+    return;
+
+  this->write_register(THRESHOLD, NOISE_FLOOR_MASK, noise_level, 4);
+}
+// REG0x02, bits [3:0], manufacturer default: 0010 (2).
+// This setting, like the watchdog threshold, can help determine between false
+// events and actual lightning. The shape of the spike is analyzed during the
+// chip's signal validation routine. Increasing this value increases robustness
+// at the cost of sensitivity to distant events.
+void AS3935Component::write_spike_rejection(uint8_t spike_rejection) {
+  ESP_LOGV(TAG, "Setting spike rejection to %d", spike_rejection);
+  if ((spike_rejection < 1) || (spike_rejection > 11))
+    return;
+
+  this->write_register(LIGHTNING_REG, SPIKE_MASK, spike_rejection, 0);
+}
+// REG0x02, bits [5:4], manufacturer default: 0 (single lightning strike).
+// The number of lightning events before IRQ is set high. 15 minutes is The
+// window of time before the number of detected lightning events is reset.
+// The number of lightning strikes can be set to 1,5,9, or 16.
+void AS3935Component::write_lightning_threshold(uint8_t lightning_threshold) {
+  ESP_LOGV(TAG, "Setting lightning threshold to %d", lightning_threshold);
+  switch (lightning_threshold) {
+    case 1:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 0, 4);  // Demonstrative
+      break;
+    case 5:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 4);
+      break;
+    case 9:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 5);
+      break;
+    case 16:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 3, 4);
+      break;
+    default:
+      return;
+  }
+}
+// REG0x03, bit [5], manufacturer default: 0.
+// This setting will return whether or not disturbers trigger the IRQ Pin.
+void AS3935Component::write_mask_disturber(bool enabled) {
+  ESP_LOGV(TAG, "Setting mask disturber to %d", enabled);
+  if (enabled) {
+    this->write_register(INT_MASK_ANT, (1 << 5), 1, 5);
+  } else {
+    this->write_register(INT_MASK_ANT, (1 << 5), 0, 5);
+  }
+}
+// REG0x03, bit [7:6], manufacturer default: 0 (16 division ratio).
+// The antenna is designed to resonate at 500kHz and so can be tuned with the
+// following setting. The accuracy of the antenna must be within 3.5 percent of
+// that value for proper signal validation and distance estimation.
+void AS3935Component::write_div_ratio(uint8_t div_ratio) {
+  ESP_LOGV(TAG, "Setting div ratio to %d", div_ratio);
+  switch (div_ratio) {
+    case 16:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 0, 6);
+      break;
+    case 22:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 6);
+      break;
+    case 64:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 7);
+      break;
+    case 128:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 3, 6);
+      break;
+    default:
+      return;
+  }
+}
+// REG0x08, bits [3:0], manufacturer default: 0.
+// This setting will add capacitance to the series RLC antenna on the product
+// to help tune its resonance. The datasheet specifies being within 3.5 percent
+// of 500kHz to get optimal lightning detection and distance sensing.
+// It's possible to add up to 120pF in steps of 8pF to the antenna.
+void AS3935Component::write_capacitance(uint8_t capacitance) {
+  ESP_LOGV(TAG, "Setting tune cap to %d pF", capacitance * 8);
+  this->write_register(FREQ_DISP_IRQ, CAP_MASK, capacitance, 0);
+}
+
+// REG0x03, bits [3:0], manufacturer default: 0.
+// When there is an event that exceeds the watchdog threshold, the register is written
+// with the type of event. This consists of two messages: INT_D (disturber detected) and
+// INT_L (Lightning detected). A third interrupt INT_NH (noise level too HIGH)
+// indicates that the noise level has been exceeded and will persist until the
+// noise has ended. Events are active HIGH. There is a one second window of time to
+// read the interrupt register after lightning is detected, and 1.5 after
+// disturber.
+uint8_t AS3935Component::read_interrupt_register_() {
+  // A 2ms delay is added to allow for the memory register to be populated
+  // after the interrupt pin goes HIGH. See "Interrupt Management" in
+  // datasheet.
+  ESP_LOGV(TAG, "Calling read_interrupt_register_");
+  delay(2);
+  return this->read_register_(INT_MASK_ANT, INT_MASK);
+}
+
+// REG0x02, bit [6], manufacturer default: 1.
+// This register clears the number of lightning strikes that has been read in
+// the last 15 minute block.
+void AS3935Component::clear_statistics_() {
+  // Write high, then low, then high to clear.
+  ESP_LOGV(TAG, "Calling clear_statistics_");
+  this->write_register(LIGHTNING_REG, (1 << 6), 1, 6);
+  this->write_register(LIGHTNING_REG, (1 << 6), 0, 6);
+  this->write_register(LIGHTNING_REG, (1 << 6), 1, 6);
+}
+
+// REG0x07, bit [5:0], manufacturer default: 0.
+// This register holds the distance to the front of the storm and not the
+// distance to a lightning strike.
+uint8_t AS3935Component::get_distance_to_storm_() {
+  ESP_LOGV(TAG, "Calling get_distance_to_storm_");
+  return this->read_register_(DISTANCE, DISTANCE_MASK);
+}
+
+uint32_t AS3935Component::get_lightning_energy_() {
+  ESP_LOGV(TAG, "Calling get_lightning_energy_");
+  uint32_t pure_light = 0;  // Variable for lightning energy which is just a pure number.
+  uint32_t temp = 0;
+  // Temp variable for lightning energy.
+  temp = this->read_register_(ENERGY_LIGHT_MMSB, ENERGY_MASK);
+  // Temporary Value is large enough to handle a shift of 16 bits.
+  pure_light = temp << 16;
+  temp = this->read_register(ENERGY_LIGHT_MSB);
+  // Temporary value is large enough to handle a shift of 8 bits.
+  pure_light |= temp << 8;
+  // No shift here, directly OR'ed into pure_light variable.
+  temp = this->read_register(ENERGY_LIGHT_LSB);
+  pure_light |= temp;
+  return pure_light;
+}
+
+uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) {
+  uint8_t value = this->read_register(reg);
+  value &= (~mask);
+  return value;
+}
+
+}  // namespace as3935
+}  // namespace esphome
diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h
new file mode 100644
index 0000000000..d0e53e7832
--- /dev/null
+++ b/esphome/components/as3935/as3935.h
@@ -0,0 +1,110 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace as3935 {
+
+enum AS3935RegisterNames {
+  AFE_GAIN = 0x00,
+  THRESHOLD,
+  LIGHTNING_REG,
+  INT_MASK_ANT,
+  ENERGY_LIGHT_LSB,
+  ENERGY_LIGHT_MSB,
+  ENERGY_LIGHT_MMSB,
+  DISTANCE,
+  FREQ_DISP_IRQ,
+  CALIB_TRCO = 0x3A,
+  CALIB_SRCO = 0x3B,
+  DEFAULT_RESET = 0x3C,
+  CALIB_RCO = 0x3D
+};
+
+enum AS3935RegisterMasks {
+  GAIN_MASK = 0x3E,
+  SPIKE_MASK = 0xF,
+  IO_MASK = 0xC1,
+  DISTANCE_MASK = 0xC0,
+  INT_MASK = 0xF0,
+  THRESH_MASK = 0x0F,
+  R_SPIKE_MASK = 0xF0,
+  ENERGY_MASK = 0xF0,
+  CAP_MASK = 0xF0,
+  LIGHT_MASK = 0xCF,
+  DISTURB_MASK = 0xDF,
+  NOISE_FLOOR_MASK = 0x70,
+  OSC_MASK = 0xE0,
+  CALIB_MASK = 0x7F,
+  DIV_MASK = 0x3F
+};
+
+enum AS3935Values {
+  AS3935_ADDR = 0x03,
+  INDOOR = 0x12,
+  OUTDOOR = 0xE,
+  LIGHTNING_INT = 0x08,
+  DISTURBER_INT = 0x04,
+  NOISE_INT = 0x01
+};
+
+class AS3935Component : public Component {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void loop() override;
+
+  void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; }
+  void set_distance_sensor(sensor::Sensor *distance_sensor) { distance_sensor_ = distance_sensor; }
+  void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
+  void set_thunder_alert_binary_sensor(binary_sensor::BinarySensor *thunder_alert_binary_sensor) {
+    thunder_alert_binary_sensor_ = thunder_alert_binary_sensor;
+  }
+  void set_indoor(bool indoor) { indoor_ = indoor; }
+  void write_indoor(bool indoor);
+  void set_noise_level(uint8_t noise_level) { noise_level_ = noise_level; }
+  void write_noise_level(uint8_t noise_level);
+  void set_watchdog_threshold(uint8_t watchdog_threshold) { watchdog_threshold_ = watchdog_threshold; }
+  void write_watchdog_threshold(uint8_t watchdog_threshold);
+  void set_spike_rejection(uint8_t spike_rejection) { spike_rejection_ = spike_rejection; }
+  void write_spike_rejection(uint8_t write_spike_rejection);
+  void set_lightning_threshold(uint8_t lightning_threshold) { lightning_threshold_ = lightning_threshold; }
+  void write_lightning_threshold(uint8_t lightning_threshold);
+  void set_mask_disturber(bool mask_disturber) { mask_disturber_ = mask_disturber; }
+  void write_mask_disturber(bool enabled);
+  void set_div_ratio(uint8_t div_ratio) { div_ratio_ = div_ratio; }
+  void write_div_ratio(uint8_t div_ratio);
+  void set_capacitance(uint8_t capacitance) { capacitance_ = capacitance; }
+  void write_capacitance(uint8_t capacitance);
+
+ protected:
+  uint8_t read_interrupt_register_();
+  void clear_statistics_();
+  uint8_t get_distance_to_storm_();
+  uint32_t get_lightning_energy_();
+
+  virtual uint8_t read_register(uint8_t reg) = 0;
+  uint8_t read_register_(uint8_t reg, uint8_t mask);
+
+  virtual void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) = 0;
+
+  sensor::Sensor *distance_sensor_;
+  sensor::Sensor *energy_sensor_;
+  binary_sensor::BinarySensor *thunder_alert_binary_sensor_;
+  GPIOPin *irq_pin_;
+
+  bool indoor_;
+  uint8_t noise_level_;
+  uint8_t watchdog_threshold_;
+  uint8_t spike_rejection_;
+  uint8_t lightning_threshold_;
+  bool mask_disturber_;
+  uint8_t div_ratio_;
+  uint8_t capacitance_;
+};
+
+}  // namespace as3935
+}  // namespace esphome
diff --git a/esphome/components/as3935/binary_sensor.py b/esphome/components/as3935/binary_sensor.py
new file mode 100644
index 0000000000..3748c3484a
--- /dev/null
+++ b/esphome/components/as3935/binary_sensor.py
@@ -0,0 +1,16 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from . import AS3935, CONF_AS3935_ID
+
+DEPENDENCIES = ['as3935']
+
+CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
+    cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
+})
+
+
+def to_code(config):
+    hub = yield cg.get_variable(config[CONF_AS3935_ID])
+    var = yield binary_sensor.new_binary_sensor(config)
+    cg.add(hub.set_thunder_alert_binary_sensor(var))
diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py
new file mode 100644
index 0000000000..3374ada6a8
--- /dev/null
+++ b/esphome/components/as3935/sensor.py
@@ -0,0 +1,30 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import CONF_DISTANCE, CONF_LIGHTNING_ENERGY, \
+    UNIT_KILOMETER, UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH
+from . import AS3935, CONF_AS3935_ID
+
+DEPENDENCIES = ['as3935']
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
+    cv.Optional(CONF_DISTANCE):
+        sensor.sensor_schema(UNIT_KILOMETER, ICON_SIGNAL_DISTANCE_VARIANT, 1),
+    cv.Optional(CONF_LIGHTNING_ENERGY):
+        sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 1),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    hub = yield cg.get_variable(config[CONF_AS3935_ID])
+
+    if CONF_DISTANCE in config:
+        conf = config[CONF_DISTANCE]
+        distance_sensor = yield sensor.new_sensor(conf)
+        cg.add(hub.set_distance_sensor(distance_sensor))
+
+    if CONF_LIGHTNING_ENERGY in config:
+        conf = config[CONF_LIGHTNING_ENERGY]
+        lightning_energy_sensor = yield sensor.new_sensor(conf)
+        cg.add(hub.set_distance_sensor(lightning_energy_sensor))
diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py
new file mode 100644
index 0000000000..e22937ab81
--- /dev/null
+++ b/esphome/components/as3935_i2c/__init__.py
@@ -0,0 +1,20 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import as3935, i2c
+from esphome.const import CONF_ID
+
+AUTO_LOAD = ['as3935']
+DEPENDENCIES = ['i2c']
+
+as3935_i2c_ns = cg.esphome_ns.namespace('as3935_i2c')
+I2CAS3935 = as3935_i2c_ns.class_('I2CAS3935Component', as3935.AS3935, i2c.I2CDevice)
+
+CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(I2CAS3935),
+}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x03)))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield as3935.setup_as3935(var, config)
+    yield i2c.register_i2c_device(var, config)
diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp
new file mode 100644
index 0000000000..a522116815
--- /dev/null
+++ b/esphome/components/as3935_i2c/as3935_i2c.cpp
@@ -0,0 +1,40 @@
+#include "as3935_i2c.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace as3935_i2c {
+
+static const char *TAG = "as3935_i2c";
+
+void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) {
+  uint8_t write_reg;
+  if (!this->read_byte(reg, &write_reg)) {
+    this->mark_failed();
+    ESP_LOGW(TAG, "read_byte failed - increase log level for more details!");
+    return;
+  }
+
+  write_reg &= (~mask);
+  write_reg |= (bits << start_pos);
+
+  if (!this->write_byte(reg, write_reg)) {
+    ESP_LOGW(TAG, "write_byte failed - increase log level for more details!");
+    return;
+  }
+}
+
+uint8_t I2CAS3935Component::read_register(uint8_t reg) {
+  uint8_t value;
+  if (!this->read_byte(reg, &value, 2)) {
+    ESP_LOGW(TAG, "Read failed!");
+    return 0;
+  }
+  return value;
+}
+void I2CAS3935Component::dump_config() {
+  AS3935Component::dump_config();
+  LOG_I2C_DEVICE(this);
+}
+
+}  // namespace as3935_i2c
+}  // namespace esphome
diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h
new file mode 100644
index 0000000000..1d16397bdf
--- /dev/null
+++ b/esphome/components/as3935_i2c/as3935_i2c.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/as3935/as3935.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace as3935_i2c {
+
+class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice {
+ public:
+  void dump_config() override;
+
+ protected:
+  void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override;
+  uint8_t read_register(uint8_t reg) override;
+};
+
+}  // namespace as3935_i2c
+}  // namespace esphome
diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py
new file mode 100644
index 0000000000..fa27c2b0f5
--- /dev/null
+++ b/esphome/components/as3935_spi/__init__.py
@@ -0,0 +1,20 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import as3935, spi
+from esphome.const import CONF_ID
+
+AUTO_LOAD = ['as3935']
+DEPENDENCIES = ['spi']
+
+as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi')
+SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice)
+
+CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(SPIAS3935)
+}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield as3935.setup_as3935(var, config)
+    yield spi.register_spi_device(var, config)
diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp
new file mode 100644
index 0000000000..73752a8ee0
--- /dev/null
+++ b/esphome/components/as3935_spi/as3935_spi.cpp
@@ -0,0 +1,48 @@
+#include "as3935_spi.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace as3935_spi {
+
+static const char *TAG = "as3935_spi";
+
+void SPIAS3935Component::setup() {
+  ESP_LOGI(TAG, "SPIAS3935Component setup started!");
+  this->spi_setup();
+  ESP_LOGI(TAG, "SPI setup finished!");
+  AS3935Component::setup();
+}
+
+void SPIAS3935Component::dump_config() {
+  AS3935Component::dump_config();
+  LOG_PIN("  CS Pin: ", this->cs_);
+}
+
+void SPIAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) {
+  uint8_t write_reg = this->read_register(reg);
+
+  write_reg &= (~mask);
+  write_reg |= (bits << start_pos);
+
+  this->enable();
+  this->write_byte(reg);
+  this->write_byte(write_reg);
+  this->disable();
+}
+
+uint8_t SPIAS3935Component::read_register(uint8_t reg) {
+  uint8_t value = 0;
+  this->enable();
+  this->write_byte(reg |= SPI_READ_M);
+  value = this->read_byte();
+  // According to datsheet, the chip select must be written HIGH, LOW, HIGH
+  // to correctly end the READ command.
+  this->cs_->digital_write(true);
+  this->cs_->digital_write(false);
+  this->disable();
+  ESP_LOGV(TAG, "read_register_: %d", value);
+  return value;
+}
+
+}  // namespace as3935_spi
+}  // namespace esphome
diff --git a/esphome/components/as3935_spi/as3935_spi.h b/esphome/components/as3935_spi/as3935_spi.h
new file mode 100644
index 0000000000..073f5c09a4
--- /dev/null
+++ b/esphome/components/as3935_spi/as3935_spi.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/as3935/as3935.h"
+#include "esphome/components/spi/spi.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace as3935_spi {
+
+enum AS3935RegisterMasks { SPI_READ_M = 0x40 };
+
+class SPIAS3935Component : public as3935::AS3935Component,
+                           public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                                 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
+ public:
+  void setup() override;
+  void dump_config() override;
+
+ protected:
+  void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override;
+  uint8_t read_register(uint8_t reg) override;
+};
+
+}  // namespace as3935_spi
+}  // namespace esphome
diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py
new file mode 100644
index 0000000000..4cbd5358ce
--- /dev/null
+++ b/esphome/components/async_tcp/__init__.py
@@ -0,0 +1,23 @@
+# Dummy integration to allow relying on AsyncTCP
+import esphome.codegen as cg
+from esphome.const import ARDUINO_VERSION_ESP32_1_0_0, ARDUINO_VERSION_ESP32_1_0_1, \
+    ARDUINO_VERSION_ESP32_1_0_2
+from esphome.core import CORE, coroutine_with_priority
+
+
+@coroutine_with_priority(200.0)
+def to_code(config):
+    if CORE.is_esp32:
+        # https://github.com/me-no-dev/AsyncTCP/blob/master/library.json
+        versions_requiring_older_asynctcp = [
+            ARDUINO_VERSION_ESP32_1_0_0,
+            ARDUINO_VERSION_ESP32_1_0_1,
+            ARDUINO_VERSION_ESP32_1_0_2,
+        ]
+        if CORE.arduino_version in versions_requiring_older_asynctcp:
+            cg.add_library('AsyncTCP', '1.0.3')
+        else:
+            cg.add_library('AsyncTCP', '1.1.1')
+    elif CORE.is_esp8266:
+        # https://github.com/OttoWinter/ESPAsyncTCP
+        cg.add_library('ESPAsyncTCP-esphome', '1.2.2')
diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp
new file mode 100644
index 0000000000..283319994e
--- /dev/null
+++ b/esphome/components/atm90e32/atm90e32.cpp
@@ -0,0 +1,188 @@
+#include "atm90e32.h"
+#include "atm90e32_reg.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace atm90e32 {
+
+static const char *TAG = "atm90e32";
+
+void ATM90E32Component::update() {
+  if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) {
+    this->status_set_warning();
+    return;
+  }
+
+  if (this->phase_[0].voltage_sensor_ != nullptr) {
+    this->phase_[0].voltage_sensor_->publish_state(this->get_line_voltage_a_());
+  }
+  if (this->phase_[1].voltage_sensor_ != nullptr) {
+    this->phase_[1].voltage_sensor_->publish_state(this->get_line_voltage_b_());
+  }
+  if (this->phase_[2].voltage_sensor_ != nullptr) {
+    this->phase_[2].voltage_sensor_->publish_state(this->get_line_voltage_c_());
+  }
+  if (this->phase_[0].current_sensor_ != nullptr) {
+    this->phase_[0].current_sensor_->publish_state(this->get_line_current_a_());
+  }
+  if (this->phase_[1].current_sensor_ != nullptr) {
+    this->phase_[1].current_sensor_->publish_state(this->get_line_current_b_());
+  }
+  if (this->phase_[2].current_sensor_ != nullptr) {
+    this->phase_[2].current_sensor_->publish_state(this->get_line_current_c_());
+  }
+  if (this->phase_[0].power_sensor_ != nullptr) {
+    this->phase_[0].power_sensor_->publish_state(this->get_active_power_a_());
+  }
+  if (this->phase_[1].power_sensor_ != nullptr) {
+    this->phase_[1].power_sensor_->publish_state(this->get_active_power_b_());
+  }
+  if (this->phase_[2].power_sensor_ != nullptr) {
+    this->phase_[2].power_sensor_->publish_state(this->get_active_power_c_());
+  }
+  if (this->freq_sensor_ != nullptr) {
+    this->freq_sensor_->publish_state(this->get_frequency_());
+  }
+  this->status_clear_warning();
+}
+
+void ATM90E32Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up ATM90E32Component...");
+  this->spi_setup();
+
+  uint16_t mmode0 = 0x185;
+  if (line_freq_ == 60) {
+    mmode0 |= 1 << 12;
+  }
+
+  this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A);    // Perform soft reset
+  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);  // enable register config access
+  this->write16_(ATM90E32_REGISTER_METEREN, 0x0001);      // Enable Metering
+  if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x0001) {
+    ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
+    this->mark_failed();
+    return;
+  }
+
+  this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0x0A55);                    // ZX2, ZX1, ZX0 pin config
+  this->write16_(ATM90E32_REGISTER_MMODE0, mmode0);                      // Mode Config (frequency set in main program)
+  this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_);                   // PGA Gain Configuration for Current Channels
+  this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x0AFC);                    // Active Startup Power Threshold = 50%
+  this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x0AEC);                    // Reactive Startup Power Threshold = 50%
+  this->write16_(ATM90E32_REGISTER_PPHASETH, 0x00BC);                    // Active Phase Threshold = 10%
+  this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[0].volt_gain_);  // A Voltage rms gain
+  this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[0].ct_gain_);    // A line current gain
+  this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[1].volt_gain_);  // B Voltage rms gain
+  this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[1].ct_gain_);    // B line current gain
+  this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[2].volt_gain_);  // C Voltage rms gain
+  this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[2].ct_gain_);    // C line current gain
+  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);                 // end configuration
+}
+
+void ATM90E32Component::dump_config() {
+  ESP_LOGCONFIG("", "ATM90E32:");
+  LOG_PIN("  CS Pin: ", this->cs_);
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Communication with ATM90E32 failed!");
+  }
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "Voltage A", this->phase_[0].voltage_sensor_);
+  LOG_SENSOR("  ", "Current A", this->phase_[0].current_sensor_);
+  LOG_SENSOR("  ", "Power A", this->phase_[0].power_sensor_);
+  LOG_SENSOR("  ", "Voltage B", this->phase_[1].voltage_sensor_);
+  LOG_SENSOR("  ", "Current B", this->phase_[1].current_sensor_);
+  LOG_SENSOR("  ", "Power B", this->phase_[1].power_sensor_);
+  LOG_SENSOR("  ", "Voltage C", this->phase_[2].voltage_sensor_);
+  LOG_SENSOR("  ", "Current C", this->phase_[2].current_sensor_);
+  LOG_SENSOR("  ", "Power C", this->phase_[2].power_sensor_);
+  LOG_SENSOR("  ", "Frequency", this->freq_sensor_)
+}
+float ATM90E32Component::get_setup_priority() const { return setup_priority::DATA; }
+
+uint16_t ATM90E32Component::read16_(uint16_t a_register) {
+  uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
+  uint8_t addrl = (a_register & 0xFF);
+  uint8_t data[2];
+  uint16_t output;
+
+  this->enable();
+  delayMicroseconds(10);
+  this->write_byte(addrh);
+  this->write_byte(addrl);
+  delayMicroseconds(4);
+  this->read_array(data, 2);
+  this->disable();
+
+  output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
+  ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output);
+  return output;
+}
+
+int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
+  uint16_t val_h = this->read16_(addr_h);
+  uint16_t val_l = this->read16_(addr_l);
+  int32_t val = (val_h << 16) | val_l;
+
+  ESP_LOGVV(TAG, "read32_ addr_h 0x%04X val_h 0x%04X addr_l 0x%04X val_l 0x%04X = %d", addr_h, val_h, addr_l, val_l,
+            val);
+
+  return val;
+}
+
+void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
+  uint8_t addrh = (a_register >> 8) & 0x03;
+  uint8_t addrl = (a_register & 0xFF);
+
+  ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val);
+  this->enable();
+  delayMicroseconds(10);
+  this->write_byte(addrh);
+  this->write_byte(addrl);
+  delayMicroseconds(4);
+  this->write_byte((val >> 8) & 0xff);
+  this->write_byte(val & 0xFF);
+  this->disable();
+}
+
+float ATM90E32Component::get_line_voltage_a_() {
+  uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSA);
+  return (float) voltage / 100;
+}
+float ATM90E32Component::get_line_voltage_b_() {
+  uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSB);
+  return (float) voltage / 100;
+}
+float ATM90E32Component::get_line_voltage_c_() {
+  uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSC);
+  return (float) voltage / 100;
+}
+float ATM90E32Component::get_line_current_a_() {
+  uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSA);
+  return (float) current / 1000;
+}
+float ATM90E32Component::get_line_current_b_() {
+  uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSB);
+  return (float) current / 1000;
+}
+float ATM90E32Component::get_line_current_c_() {
+  uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSC);
+  return (float) current / 1000;
+}
+float ATM90E32Component::get_active_power_a_() {
+  int val = this->read32_(ATM90E32_REGISTER_PMEANA, ATM90E32_REGISTER_PMEANALSB);
+  return val * 0.00032f;
+}
+float ATM90E32Component::get_active_power_b_() {
+  int val = this->read32_(ATM90E32_REGISTER_PMEANB, ATM90E32_REGISTER_PMEANBLSB);
+  return val * 0.00032f;
+}
+float ATM90E32Component::get_active_power_c_() {
+  int val = this->read32_(ATM90E32_REGISTER_PMEANC, ATM90E32_REGISTER_PMEANCLSB);
+  return val * 0.00032f;
+}
+float ATM90E32Component::get_frequency_() {
+  uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ);
+  return (float) freq / 100;
+}
+}  // namespace atm90e32
+}  // namespace esphome
diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h
new file mode 100644
index 0000000000..4dd2bd5784
--- /dev/null
+++ b/esphome/components/atm90e32/atm90e32.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/spi/spi.h"
+
+namespace esphome {
+namespace atm90e32 {
+
+class ATM90E32Component : public PollingComponent,
+                          public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
+                                                spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void update() override;
+
+  void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; }
+  void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; }
+  void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; }
+  void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; }
+  void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
+
+  void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
+  void set_line_freq(int freq) { line_freq_ = freq; }
+  void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
+
+ protected:
+  uint16_t read16_(uint16_t a_register);
+  int read32_(uint16_t addr_h, uint16_t addr_l);
+  void write16_(uint16_t a_register, uint16_t val);
+
+  float get_line_voltage_a_();
+  float get_line_voltage_b_();
+  float get_line_voltage_c_();
+  float get_line_current_a_();
+  float get_line_current_b_();
+  float get_line_current_c_();
+  float get_active_power_a_();
+  float get_active_power_b_();
+  float get_active_power_c_();
+  float get_frequency_();
+
+  struct ATM90E32Phase {
+    uint16_t volt_gain_{41820};
+    uint16_t ct_gain_{25498};
+    sensor::Sensor *voltage_sensor_{nullptr};
+    sensor::Sensor *current_sensor_{nullptr};
+    sensor::Sensor *power_sensor_{nullptr};
+  } phase_[3];
+  sensor::Sensor *freq_sensor_{nullptr};
+  uint16_t pga_gain_{0x15};
+  int line_freq_{60};
+};
+
+}  // namespace atm90e32
+}  // namespace esphome
diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h
new file mode 100644
index 0000000000..ca3715a2a8
--- /dev/null
+++ b/esphome/components/atm90e32/atm90e32_reg.h
@@ -0,0 +1,253 @@
+#pragma once
+
+namespace esphome {
+namespace atm90e32 {
+
+/* STATUS REGISTERS */
+static const uint16_t ATM90E32_REGISTER_METEREN = 0x00;        // Metering Enable
+static const uint16_t ATM90E32_REGISTER_CHANNELMAPI = 0x01;    // Current Channel Mapping Configuration
+static const uint16_t ATM90E32_REGISTER_CHANNELMAPU = 0x02;    // Voltage Channel Mapping Configuration
+static const uint16_t ATM90E32_REGISTER_SAGPEAKDETCFG = 0x05;  // Sag and Peak Detector Period Configuration
+static const uint16_t ATM90E32_REGISTER_OVTH = 0x06;           // Over Voltage Threshold
+static const uint16_t ATM90E32_REGISTER_ZXCONFIG = 0x07;       // Zero-Crossing Config
+static const uint16_t ATM90E32_REGISTER_SAGTH = 0x08;          // Voltage Sag Th
+static const uint16_t ATM90E32_REGISTER_PHASELOSSTH = 0x09;    // Voltage Phase Losing Th
+static const uint16_t ATM90E32_REGISTER_INWARNTH = 0x0A;       // Neutral Current (Calculated) Warning Threshold
+static const uint16_t ATM90E32_REGISTER_OITH = 0x0B;           // Over Current Threshold
+static const uint16_t ATM90E32_REGISTER_FREQLOTH = 0x0C;       // Low Threshold for Frequency Detection
+static const uint16_t ATM90E32_REGISTER_FREQHITH = 0x0D;       // High Threshold for Frequency Detection
+static const uint16_t ATM90E32_REGISTER_PMPWRCTRL = 0x0E;      // Partial Measurement Mode Power Control
+static const uint16_t ATM90E32_REGISTER_IRQ0MERGECFG = 0x0F;   // IRQ0 Merge Configuration
+
+/* EMM STATUS REGISTERS */
+static const uint16_t ATM90E32_REGISTER_SOFTRESET = 0x70;       // Software Reset
+static const uint16_t ATM90E32_REGISTER_EMMSTATE0 = 0x71;       // EMM State 0
+static const uint16_t ATM90E32_REGISTER_EMMSTATE1 = 0x72;       // EMM State 1
+static const uint16_t ATM90E32_REGISTER_EMMINTSTATE0 = 0x73;    // EMM Interrupt Status 0
+static const uint16_t ATM90E32_REGISTER_EMMINTSTATE1 = 0x74;    // EMM Interrupt Status 1
+static const uint16_t ATM90E32_REGISTER_EMMINTEN0 = 0x75;       // EMM Interrupt Enable 0
+static const uint16_t ATM90E32_REGISTER_EMMINTEN1 = 0x76;       // EMM Interrupt Enable 1
+static const uint16_t ATM90E32_REGISTER_LASTSPIDATA = 0x78;     // Last Read/Write SPI Value
+static const uint16_t ATM90E32_REGISTER_CRCERRSTATUS = 0x79;    // CRC Error Status
+static const uint16_t ATM90E32_REGISTER_CRCDIGEST = 0x7A;       // CRC Digest
+static const uint16_t ATM90E32_REGISTER_CFGREGACCEN = 0x7F;     // Configure Register Access Enable
+static const uint16_t ATM90E32_STATUS_S0_OIPHASEAST = 1 << 15;  // Over current on phase A
+static const uint16_t ATM90E32_STATUS_S0_OIPHASEBST = 1 << 14;  // Over current on phase B
+static const uint16_t ATM90E32_STATUS_S0_OIPHASECST = 1 << 13;  // Over current on phase C
+static const uint16_t ATM90E32_STATUS_S0_OVPHASEAST = 1 << 12;  // Over voltage on phase A
+static const uint16_t ATM90E32_STATUS_S0_OVPHASEBST = 1 << 11;  // Over voltage on phase B
+static const uint16_t ATM90E32_STATUS_S0_OVPHASECST = 1 << 10;  // Over voltage on phase C
+static const uint16_t ATM90E32_STATUS_S0_UREVWNST = 1 << 9;     // Voltage Phase Sequence Error status
+static const uint16_t ATM90E32_STATUS_S0_IREVWNST = 1 << 8;     // Current Phase Sequence Error status
+static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7;      // Calculated N line current greater tha INWarnTh reg
+static const uint16_t ATM90E32_STATUS_S0_TQNOLOADST = 1 << 6;   // All phase sum reactive power no-load condition status
+static const uint16_t ATM90E32_STATUS_S0_TPNOLOADST = 1 << 5;   // All phase sum active power no-load condition status
+static const uint16_t ATM90E32_STATUS_S0_TASNOLOADST = 1 << 4;  // All phase sum apparent power no-load status
+static const uint16_t ATM90E32_STATUS_S0_CF1REVST = 1 << 3;     // Energy for CF1 Forward/Reverse status
+static const uint16_t ATM90E32_STATUS_S0_CF2REVST = 1 << 2;     // Energy for CF2 Forward/Reverse status
+static const uint16_t ATM90E32_STATUS_S0_CF3REVST = 1 << 1;     // Energy for CF3 Forward/Reverse status
+static const uint16_t ATM90E32_STATUS_S0_CF4REVST = 1 << 0;     // Energy for CF4 Forward/Reverse status
+static const uint16_t ATM90E32_STATUS_S1_FREQHIST = 1 << 15;    // Frequency is greater than the high threshold
+static const uint16_t ATM90E32_STATUS_S1_SAGPHASEAST = 1 << 14;   // Voltage sag on phase A
+static const uint16_t ATM90E32_STATUS_S1_SAGPHASEBST = 1 << 13;   // Voltage sag on phase B
+static const uint16_t ATM90E32_STATUS_S1_SAGPHASECST = 1 << 12;   // Voltage sag on phase C
+static const uint16_t ATM90E32_STATUS_S1_FREQLOST = 1 << 11;      // Frequency is lesser than the low threshold
+static const uint16_t ATM90E32_STATUS_S1_PHASELOSSAST = 1 << 10;  // Phase loss in Phase A
+static const uint16_t ATM90E32_STATUS_S1_PHASELOSSBST = 1 << 9;   // Phase loss in Phase B
+static const uint16_t ATM90E32_STATUS_S1_PHASELOSSCST = 1 << 8;   // Phase loss in Phase C
+static const uint16_t ATM90E32_STATUS_S1_QEREGTPST = 1 << 7;      // ReActive Energy register of sum (T) Positive Status
+static const uint16_t ATM90E32_STATUS_S1_QEREGAPST = 1 << 6;  // ReActive Energy register of Channel A Positive Status
+static const uint16_t ATM90E32_STATUS_S1_QEREGBPST = 1 << 5;  // ReActive Energy register of Channel B Positive Status
+static const uint16_t ATM90E32_STATUS_S1_QEREGCPST = 1 << 4;  // ReActive Energy register of Channel C Positive Status
+static const uint16_t ATM90E32_STATUS_S1_PEREGTPST = 1 << 3;  // Active Energy register of sum (T) Positive Status
+static const uint16_t ATM90E32_STATUS_S1_PEREGAPST = 1 << 2;  // Active Energy register of Channel A Positive Status
+static const uint16_t ATM90E32_STATUS_S1_PEREGBPST = 1 << 1;  // Active Energy register of Channel B Positive Status
+static const uint16_t ATM90E32_STATUS_S1_PEREGCPST = 1 << 0;  // Active Energy register of Channel C Positive Status
+
+/* LOW POWER MODE REGISTERS - NOT USED */
+static const uint16_t ATM90E32_REGISTER_DETECTCTRL = 0x10;
+static const uint16_t ATM90E32_REGISTER_DETECTTH1 = 0x11;
+static const uint16_t ATM90E32_REGISTER_DETECTTH2 = 0x12;
+static const uint16_t ATM90E32_REGISTER_DETECTTH3 = 0x13;
+static const uint16_t ATM90E32_REGISTER_PMOFFSETA = 0x14;
+static const uint16_t ATM90E32_REGISTER_PMOFFSETB = 0x15;
+static const uint16_t ATM90E32_REGISTER_PMOFFSETC = 0x16;
+static const uint16_t ATM90E32_REGISTER_PMPGA = 0x17;
+static const uint16_t ATM90E32_REGISTER_PMIRMSA = 0x18;
+static const uint16_t ATM90E32_REGISTER_PMIRMSB = 0x19;
+static const uint16_t ATM90E32_REGISTER_PMIRMSC = 0x1A;
+static const uint16_t ATM90E32_REGISTER_PMCONFIG = 0x10B;
+static const uint16_t ATM90E32_REGISTER_PMAVGSAMPLES = 0x1C;
+static const uint16_t ATM90E32_REGISTER_PMIRMSLSB = 0x1D;
+
+/* CONFIGURATION REGISTERS */
+static const uint16_t ATM90E32_REGISTER_PLCONSTH = 0x31;  // High Word of PL_Constant
+static const uint16_t ATM90E32_REGISTER_PLCONSTL = 0x32;  // Low Word of PL_Constant
+static const uint16_t ATM90E32_REGISTER_MMODE0 = 0x33;    // Metering Mode Config
+static const uint16_t ATM90E32_REGISTER_MMODE1 = 0x34;    // PGA Gain Configuration for Current Channels
+static const uint16_t ATM90E32_REGISTER_PSTARTTH = 0x35;  // Startup Power Th (P)
+static const uint16_t ATM90E32_REGISTER_QSTARTTH = 0x36;  // Startup Power Th (Q)
+static const uint16_t ATM90E32_REGISTER_SSTARTTH = 0x37;  // Startup Power Th (S)
+static const uint16_t ATM90E32_REGISTER_PPHASETH = 0x38;  // Startup Power Accum Th (P)
+static const uint16_t ATM90E32_REGISTER_QPHASETH = 0x39;  // Startup Power Accum Th (Q)
+static const uint16_t ATM90E32_REGISTER_SPHASETH = 0x3A;  // Startup Power Accum Th (S)
+
+/* CALIBRATION REGISTERS */
+static const uint16_t ATM90E32_REGISTER_POFFSETA = 0x41;  // A Line Power Offset (P)
+static const uint16_t ATM90E32_REGISTER_QOFFSETA = 0x42;  // A Line Power Offset (Q)
+static const uint16_t ATM90E32_REGISTER_POFFSETB = 0x43;  // B Line Power Offset (P)
+static const uint16_t ATM90E32_REGISTER_QOFFSETB = 0x44;  // B Line Power Offset (Q)
+static const uint16_t ATM90E32_REGISTER_POFFSETC = 0x45;  // C Line Power Offset (P)
+static const uint16_t ATM90E32_REGISTER_QOFFSETC = 0x46;  // C Line Power Offset (Q)
+static const uint16_t ATM90E32_REGISTER_PQGAINA = 0x47;   // A Line Calibration Gain
+static const uint16_t ATM90E32_REGISTER_PHIA = 0x48;      // A Line Calibration Angle
+static const uint16_t ATM90E32_REGISTER_PQGAINB = 0x49;   // B Line Calibration Gain
+static const uint16_t ATM90E32_REGISTER_PHIB = 0x4A;      // B Line Calibration Angle
+static const uint16_t ATM90E32_REGISTER_PQGAINC = 0x4B;   // C Line Calibration Gain
+static const uint16_t ATM90E32_REGISTER_PHIC = 0x4C;      // C Line Calibration Angle
+
+/* FUNDAMENTAL/HARMONIC ENERGY CALIBRATION REGISTERS */
+static const uint16_t ATM90E32_REGISTER_POFFSETAF = 0x51;  // A Fund Power Offset (P)
+static const uint16_t ATM90E32_REGISTER_POFFSETBF = 0x52;  // B Fund Power Offset (P)
+static const uint16_t ATM90E32_REGISTER_POFFSETCF = 0x53;  // C Fund Power Offset (P)
+static const uint16_t ATM90E32_REGISTER_PGAINAF = 0x54;    // A Fund Power Gain (P)
+static const uint16_t ATM90E32_REGISTER_PGAINBF = 0x55;    // B Fund Power Gain (P)
+static const uint16_t ATM90E32_REGISTER_PGAINCF = 0x56;    // C Fund Power Gain (P)
+
+/* MEASUREMENT CALIBRATION REGISTERS */
+static const uint16_t ATM90E32_REGISTER_UGAINA = 0x61;    // A Voltage RMS Gain
+static const uint16_t ATM90E32_REGISTER_IGAINA = 0x62;    // A Current RMS Gain
+static const uint16_t ATM90E32_REGISTER_UOFFSETA = 0x63;  // A Voltage Offset
+static const uint16_t ATM90E32_REGISTER_IOFFSETA = 0x64;  // A Current Offset
+static const uint16_t ATM90E32_REGISTER_UGAINB = 0x65;    // B Voltage RMS Gain
+static const uint16_t ATM90E32_REGISTER_IGAINB = 0x66;    // B Current RMS Gain
+static const uint16_t ATM90E32_REGISTER_UOFFSETB = 0x67;  // B Voltage Offset
+static const uint16_t ATM90E32_REGISTER_IOFFSETB = 0x68;  // B Current Offset
+static const uint16_t ATM90E32_REGISTER_UGAINC = 0x69;    // C Voltage RMS Gain
+static const uint16_t ATM90E32_REGISTER_IGAINC = 0x6A;    // C Current RMS Gain
+static const uint16_t ATM90E32_REGISTER_UOFFSETC = 0x6B;  // C Voltage Offset
+static const uint16_t ATM90E32_REGISTER_IOFFSETC = 0x6C;  // C Current Offset
+static const uint16_t ATM90E32_REGISTER_IOFFSETN = 0x6E;  // N Current Offset
+
+/* ENERGY REGISTERS */
+static const uint16_t ATM90E32_REGISTER_APENERGYT = 0x80;  // Total Forward Active
+static const uint16_t ATM90E32_REGISTER_APENERGYA = 0x81;  // A Forward Active
+static const uint16_t ATM90E32_REGISTER_APENERGYB = 0x82;  // B Forward Active
+static const uint16_t ATM90E32_REGISTER_APENERGYC = 0x83;  // C Forward Active
+static const uint16_t ATM90E32_REGISTER_ANENERGYT = 0x84;  // Total Reverse Active
+static const uint16_t ATM90E32_REGISTER_ANENERGYA = 0x85;  // A Reverse Active
+static const uint16_t ATM90E32_REGISTER_ANENERGYB = 0x86;  // B Reverse Active
+static const uint16_t ATM90E32_REGISTER_ANENERGYC = 0x87;  // C Reverse Active
+static const uint16_t ATM90E32_REGISTER_RPENERGYT = 0x88;  // Total Forward Reactive
+static const uint16_t ATM90E32_REGISTER_RPENERGYA = 0x89;  // A Forward Reactive
+static const uint16_t ATM90E32_REGISTER_RPENERGYB = 0x8A;  // B Forward Reactive
+static const uint16_t ATM90E32_REGISTER_RPENERGYC = 0x8B;  // C Forward Reactive
+static const uint16_t ATM90E32_REGISTER_RNENERGYT = 0x8C;  // Total Reverse Reactive
+static const uint16_t ATM90E32_REGISTER_RNENERGYA = 0x8D;  // A Reverse Reactive
+static const uint16_t ATM90E32_REGISTER_RNENERGYB = 0x8E;  // B Reverse Reactive
+static const uint16_t ATM90E32_REGISTER_RNENERGYC = 0x8F;  // C Reverse Reactive
+
+static const uint16_t ATM90E32_REGISTER_SAENERGYT = 0x90;  // Total Apparent Energy
+static const uint16_t ATM90E32_REGISTER_SENERGYA = 0x91;   // A Apparent Energy
+static const uint16_t ATM90E32_REGISTER_SENERGYB = 0x92;   // B Apparent Energy
+static const uint16_t ATM90E32_REGISTER_SENERGYC = 0x93;   // C Apparent Energy
+
+/* FUNDAMENTAL / HARMONIC ENERGY REGISTERS */
+static const uint16_t ATM90E32_REGISTER_APENERGYTF = 0xA0;  // Total Forward Fund. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYAF = 0xA1;  // A Forward Fund. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYBF = 0xA2;  // B Forward Fund. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYCF = 0xA3;  // C Forward Fund. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYTF = 0xA4;  // Total Reverse Fund Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYAF = 0xA5;  // A Reverse Fund. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYBF = 0xA6;  // B Reverse Fund. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYCF = 0xA7;  // C Reverse Fund. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYTH = 0xA8;  // Total Forward Harm. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYAH = 0xA9;  // A Forward Harm. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYBH = 0xAA;  // B Forward Harm. Energy
+static const uint16_t ATM90E32_REGISTER_APENERGYCH = 0xAB;  // C Forward Harm. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYTH = 0xAC;  // Total Reverse Harm. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYAH = 0xAD;  // A Reverse Harm. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYBH = 0xAE;  // B Reverse Harm. Energy
+static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF;  // C Reverse Harm. Energy
+
+/* POWER & P.F. REGISTERS */
+static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0;   // Total Mean Power (P)
+static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1;   // A Mean Power (P)
+static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2;   // B Mean Power (P)
+static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3;   // C Mean Power (P)
+static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4;   // Total Mean Power (Q)
+static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5;   // A Mean Power (Q)
+static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6;   // B Mean Power (Q)
+static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7;   // C Mean Power (Q)
+static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8;   // Total Mean Power (S)
+static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9;   // A Mean Power (S)
+static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA;   // B Mean Power (S)
+static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB;   // C Mean Power (S)
+static const uint16_t ATM90E32_REGISTER_PFMEANT = 0xBC;  // Mean Power Factor
+static const uint16_t ATM90E32_REGISTER_PFMEANA = 0xBD;  // A Power Factor
+static const uint16_t ATM90E32_REGISTER_PFMEANB = 0xBE;  // B Power Factor
+static const uint16_t ATM90E32_REGISTER_PFMEANC = 0xBF;  // C Power Factor
+
+static const uint16_t ATM90E32_REGISTER_PMEANTLSB = 0xC0;   // Lower Word (Tot. Act. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANALSB = 0xC1;   // Lower Word (A Act. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANBLSB = 0xC2;   // Lower Word (B Act. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANCLSB = 0xC3;   // Lower Word (C Act. Power)
+static const uint16_t ATM90E32_REGISTER_QMEANTLSB = 0xC4;   // Lower Word (Tot. React. Power)
+static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5;   // Lower Word (A React. Power)
+static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6;   // Lower Word (B React. Power)
+static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7;   // Lower Word (C React. Power)
+static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8;  // Lower Word (Tot. App. Power)
+static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9;   // Lower Word (A App. Power)
+static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA;   // Lower Word (B App. Power)
+static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB;   // Lower Word (C App. Power)
+
+/* FUND/HARM POWER & V/I RMS REGISTERS */
+static const uint16_t ATM90E32_REGISTER_PMEANTF = 0xD0;  // Total Active Fund. Power
+static const uint16_t ATM90E32_REGISTER_PMEANAF = 0xD1;  // A Active Fund. Power
+static const uint16_t ATM90E32_REGISTER_PMEANBF = 0xD2;  // B Active Fund. Power
+static const uint16_t ATM90E32_REGISTER_PMEANCF = 0xD3;  // C Active Fund. Power
+static const uint16_t ATM90E32_REGISTER_PMEANTH = 0xD4;  // Total Active Harm. Power
+static const uint16_t ATM90E32_REGISTER_PMEANAH = 0xD5;  // A Active Harm. Power
+static const uint16_t ATM90E32_REGISTER_PMEANBH = 0xD6;  // B Active Harm. Power
+static const uint16_t ATM90E32_REGISTER_PMEANCH = 0xD7;  // C Active Harm. Power
+static const uint16_t ATM90E32_REGISTER_URMSA = 0xD9;    // A RMS Voltage
+static const uint16_t ATM90E32_REGISTER_URMSB = 0xDA;    // B RMS Voltage
+static const uint16_t ATM90E32_REGISTER_URMSC = 0xDB;    // C RMS Voltage
+static const uint16_t ATM90E32_REGISTER_IRMSA = 0xDD;    // A RMS Current
+static const uint16_t ATM90E32_REGISTER_IRMSB = 0xDE;    // B RMS Current
+static const uint16_t ATM90E32_REGISTER_IRMSC = 0xDF;    // C RMS Current
+static const uint16_t ATM90E32_REGISTER_IRMSN = 0xD8;    // Calculated N RMS Current
+
+static const uint16_t ATM90E32_REGISTER_PMEANTFLSB = 0xE0;  // Lower Word (Tot. Act. Fund. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANAFLSB = 0xE1;  // Lower Word (A Act. Fund. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANBFLSB = 0xE2;  // Lower Word (B Act. Fund. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANCFLSB = 0xE3;  // Lower Word (C Act. Fund. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANTHLSB = 0xE4;  // Lower Word (Tot. Act. Harm. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANAHLSB = 0xE5;  // Lower Word (A Act. Harm. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANBHLSB = 0xE6;  // Lower Word (B Act. Harm. Power)
+static const uint16_t ATM90E32_REGISTER_PMEANCHLSB = 0xE7;  // Lower Word (C Act. Harm. Power)
+static const uint16_t ATM90E32_REGISTER_URMSALSB = 0xE9;    // Lower Word (A RMS Voltage)
+static const uint16_t ATM90E32_REGISTER_URMSBLSB = 0xEA;    // Lower Word (B RMS Voltage)
+static const uint16_t ATM90E32_REGISTER_URMSCLSB = 0xEB;    // Lower Word (C RMS Voltage)
+static const uint16_t ATM90E32_REGISTER_IRMSALSB = 0xED;    // Lower Word (A RMS Current)
+static const uint16_t ATM90E32_REGISTER_IRMSBLSB = 0xEE;    // Lower Word (B RMS Current)
+static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF;    // Lower Word (C RMS Current)
+
+/* THD, FREQUENCY, ANGLE & TEMPTEMP REGISTERS*/
+static const uint16_t ATM90E32_REGISTER_THDNUA = 0xF1;   // A Voltage THD+N
+static const uint16_t ATM90E32_REGISTER_THDNUB = 0xF2;   // B Voltage THD+N
+static const uint16_t ATM90E32_REGISTER_THDNUC = 0xF3;   // C Voltage THD+N
+static const uint16_t ATM90E32_REGISTER_THDNIA = 0xF5;   // A Current THD+N
+static const uint16_t ATM90E32_REGISTER_THDNIB = 0xF6;   // B Current THD+N
+static const uint16_t ATM90E32_REGISTER_THDNIC = 0xF7;   // C Current THD+N
+static const uint16_t ATM90E32_REGISTER_FREQ = 0xF8;     // Frequency
+static const uint16_t ATM90E32_REGISTER_PANGLEA = 0xF9;  // A Mean Phase Angle
+static const uint16_t ATM90E32_REGISTER_PANGLEB = 0xFA;  // B Mean Phase Angle
+static const uint16_t ATM90E32_REGISTER_PANGLEC = 0xFB;  // C Mean Phase Angle
+static const uint16_t ATM90E32_REGISTER_TEMP = 0xFC;     // Measured Temperature
+static const uint16_t ATM90E32_REGISTER_UANGLEA = 0xFD;  // A Voltage Phase Angle
+static const uint16_t ATM90E32_REGISTER_UANGLEB = 0xFE;  // B Voltage Phase Angle
+static const uint16_t ATM90E32_REGISTER_UANGLEC = 0xFF;  // C Voltage Phase Angle
+
+}  // namespace atm90e32
+}  // namespace esphome
diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py
new file mode 100644
index 0000000000..7b62740f8e
--- /dev/null
+++ b/esphome/components/atm90e32/sensor.py
@@ -0,0 +1,72 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, spi
+from esphome.const import \
+    CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_FREQUENCY, \
+    ICON_FLASH, UNIT_HZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT
+
+CONF_PHASE_A = 'phase_a'
+CONF_PHASE_B = 'phase_b'
+CONF_PHASE_C = 'phase_c'
+
+CONF_LINE_FREQUENCY = 'line_frequency'
+CONF_GAIN_PGA = 'gain_pga'
+CONF_GAIN_VOLTAGE = 'gain_voltage'
+CONF_GAIN_CT = 'gain_ct'
+LINE_FREQS = {
+    '50HZ': 50,
+    '60HZ': 60,
+}
+PGA_GAINS = {
+    '1X': 0x0,
+    '2X': 0x15,
+    '4X': 0x2A,
+}
+
+atm90e32_ns = cg.esphome_ns.namespace('atm90e32')
+ATM90E32Component = atm90e32_ns.class_('ATM90E32Component', cg.PollingComponent, spi.SPIDevice)
+
+ATM90E32_PHASE_SCHEMA = cv.Schema({
+    cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2),
+    cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2),
+    cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2),
+    cv.Optional(CONF_GAIN_VOLTAGE, default=41820): cv.uint16_t,
+    cv.Optional(CONF_GAIN_CT, default=25498): cv.uint16_t,
+})
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(ATM90E32Component),
+    cv.Optional(CONF_PHASE_A): ATM90E32_PHASE_SCHEMA,
+    cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA,
+    cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA,
+    cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HZ, ICON_FLASH, 1),
+    cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
+    cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True),
+}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield spi.register_spi_device(var, config)
+
+    for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
+        if phase not in config:
+            continue
+        conf = config[phase]
+        cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
+        cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
+        if CONF_VOLTAGE in conf:
+            sens = yield sensor.new_sensor(conf[CONF_VOLTAGE])
+            cg.add(var.set_voltage_sensor(i, sens))
+        if CONF_CURRENT in conf:
+            sens = yield sensor.new_sensor(conf[CONF_CURRENT])
+            cg.add(var.set_current_sensor(i, sens))
+        if CONF_POWER in conf:
+            sens = yield sensor.new_sensor(conf[CONF_POWER])
+            cg.add(var.set_power_sensor(i, sens))
+    if CONF_FREQUENCY in config:
+        sens = yield sensor.new_sensor(config[CONF_FREQUENCY])
+        cg.add(var.set_freq_sensor(sens))
+    cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
+    cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp
index 1bdabaec37..978abae52a 100644
--- a/esphome/components/bang_bang/bang_bang_climate.cpp
+++ b/esphome/components/bang_bang/bang_bang_climate.cpp
@@ -46,52 +46,51 @@ climate::ClimateTraits BangBangClimate::traits() {
   traits.set_supports_heat_mode(this->supports_heat_);
   traits.set_supports_two_point_target_temperature(true);
   traits.set_supports_away(this->supports_away_);
+  traits.set_supports_action(true);
   return traits;
 }
 void BangBangClimate::compute_state_() {
   if (this->mode != climate::CLIMATE_MODE_AUTO) {
     // in non-auto mode
-    this->switch_to_mode_(this->mode);
+    this->switch_to_action_(static_cast<climate::ClimateAction>(this->mode));
     return;
   }
-
-  // auto mode, compute target mode
   if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) {
     // if any control values are nan, go to OFF (idle) mode
-    this->switch_to_mode_(climate::CLIMATE_MODE_OFF);
+    this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
     return;
   }
   const bool too_cold = this->current_temperature < this->target_temperature_low;
   const bool too_hot = this->current_temperature > this->target_temperature_high;
 
-  climate::ClimateMode target_mode;
+  climate::ClimateAction target_action;
   if (too_cold) {
     // too cold -> enable heating if possible, else idle
     if (this->supports_heat_)
-      target_mode = climate::CLIMATE_MODE_HEAT;
+      target_action = climate::CLIMATE_ACTION_HEATING;
     else
-      target_mode = climate::CLIMATE_MODE_OFF;
+      target_action = climate::CLIMATE_ACTION_OFF;
   } else if (too_hot) {
     // too hot -> enable cooling if possible, else idle
     if (this->supports_cool_)
-      target_mode = climate::CLIMATE_MODE_COOL;
+      target_action = climate::CLIMATE_ACTION_COOLING;
     else
-      target_mode = climate::CLIMATE_MODE_OFF;
+      target_action = climate::CLIMATE_ACTION_OFF;
   } else {
     // neither too hot nor too cold -> in range
     if (this->supports_cool_ && this->supports_heat_) {
       // if supports both ends, go to idle mode
-      target_mode = climate::CLIMATE_MODE_OFF;
+      target_action = climate::CLIMATE_ACTION_OFF;
     } else {
       // else use current mode and don't change (hysteresis)
-      target_mode = this->internal_mode_;
+      target_action = this->action;
     }
   }
 
-  this->switch_to_mode_(target_mode);
+  this->switch_to_action_(target_action);
 }
-void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) {
-  if (mode == this->internal_mode_)
+void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
+  if (action == this->action)
     // already in target mode
     return;
 
@@ -100,14 +99,14 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) {
     this->prev_trigger_ = nullptr;
   }
   Trigger<> *trig;
-  switch (mode) {
-    case climate::CLIMATE_MODE_OFF:
+  switch (action) {
+    case climate::CLIMATE_ACTION_OFF:
       trig = this->idle_trigger_;
       break;
-    case climate::CLIMATE_MODE_COOL:
+    case climate::CLIMATE_ACTION_COOLING:
       trig = this->cool_trigger_;
       break;
-    case climate::CLIMATE_MODE_HEAT:
+    case climate::CLIMATE_ACTION_HEATING:
       trig = this->heat_trigger_;
       break;
     default:
@@ -116,7 +115,7 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) {
   if (trig != nullptr) {
     // trig should never be null, but still check so that we don't crash
     trig->trigger();
-    this->internal_mode_ = mode;
+    this->action = action;
     this->prev_trigger_ = trig;
     this->publish_state();
   }
@@ -146,6 +145,14 @@ Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger
 void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
 Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
 void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
+void BangBangClimate::dump_config() {
+  LOG_CLIMATE("", "Bang Bang Climate", this);
+  ESP_LOGCONFIG(TAG, "  Supports HEAT: %s", YESNO(this->supports_heat_));
+  ESP_LOGCONFIG(TAG, "  Supports COOL: %s", YESNO(this->supports_cool_));
+  ESP_LOGCONFIG(TAG, "  Supports AWAY mode: %s", YESNO(this->supports_away_));
+  ESP_LOGCONFIG(TAG, "  Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low);
+  ESP_LOGCONFIG(TAG, "  Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high);
+}
 
 BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default;
 BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low,
diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h
index 716655d20f..84bcd51f34 100644
--- a/esphome/components/bang_bang/bang_bang_climate.h
+++ b/esphome/components/bang_bang/bang_bang_climate.h
@@ -21,6 +21,7 @@ class BangBangClimate : public climate::Climate, public Component {
  public:
   BangBangClimate();
   void setup() override;
+  void dump_config() override;
 
   void set_sensor(sensor::Sensor *sensor);
   Trigger<> *get_idle_trigger() const;
@@ -43,7 +44,7 @@ class BangBangClimate : public climate::Climate, public Component {
   void compute_state_();
 
   /// Switch the climate device to the given climate mode.
-  void switch_to_mode_(climate::ClimateMode mode);
+  void switch_to_action_(climate::ClimateAction action);
 
   /// The sensor used for getting the current temperature
   sensor::Sensor *sensor_{nullptr};
@@ -74,11 +75,6 @@ class BangBangClimate : public climate::Climate, public Component {
    * This is so that the previous trigger can be stopped before enabling a new one.
    */
   Trigger<> *prev_trigger_{nullptr};
-  /** The climate mode that is currently active - for a `.mode = AUTO` this will
-   * contain the actual mode the device
-   *
-   */
-  climate::ClimateMode internal_mode_{climate::CLIMATE_MODE_OFF};
 
   BangBangClimateTargetTempConfig normal_config_{};
   bool supports_away_{false};
diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py
index 6ba04ce355..dbfe1a8286 100644
--- a/esphome/components/binary/fan/__init__.py
+++ b/esphome/components/binary/fan/__init__.py
@@ -24,4 +24,4 @@ def to_code(config):
 
     if CONF_OSCILLATION_OUTPUT in config:
         oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
-        cg.add(var.set_oscillation(oscillation_output))
+        cg.add(var.set_oscillating(oscillation_output))
diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py
index 9f92207b19..c082e2e9af 100644
--- a/esphome/components/binary_sensor/__init__.py
+++ b/esphome/components/binary_sensor/__init__.py
@@ -23,6 +23,7 @@ IS_PLATFORM_COMPONENT = True
 
 binary_sensor_ns = cg.esphome_ns.namespace('binary_sensor')
 BinarySensor = binary_sensor_ns.class_('BinarySensor', cg.Nameable)
+BinarySensorInitiallyOff = binary_sensor_ns.class_('BinarySensorInitiallyOff', BinarySensor)
 BinarySensorPtr = BinarySensor.operator('ptr')
 
 # Triggers
@@ -41,6 +42,7 @@ BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Conditi
 
 # Filters
 Filter = binary_sensor_ns.class_('Filter')
+DelayedOnOffFilter = binary_sensor_ns.class_('DelayedOnOffFilter', Filter, cg.Component)
 DelayedOnFilter = binary_sensor_ns.class_('DelayedOnFilter', Filter, cg.Component)
 DelayedOffFilter = binary_sensor_ns.class_('DelayedOffFilter', Filter, cg.Component)
 InvertFilter = binary_sensor_ns.class_('InvertFilter', Filter)
@@ -55,6 +57,14 @@ def invert_filter_to_code(config, filter_id):
     yield cg.new_Pvariable(filter_id)
 
 
+@FILTER_REGISTRY.register('delayed_on_off', DelayedOnOffFilter,
+                          cv.positive_time_period_milliseconds)
+def delayed_on_off_filter_to_code(config, filter_id):
+    var = cg.new_Pvariable(filter_id, config)
+    yield cg.register_component(var, {})
+    yield var
+
+
 @FILTER_REGISTRY.register('delayed_on', DelayedOnFilter,
                           cv.positive_time_period_milliseconds)
 def delayed_on_filter_to_code(config, filter_id):
diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp
index 27c835d38c..1cde692dd4 100644
--- a/esphome/components/binary_sensor/binary_sensor.cpp
+++ b/esphome/components/binary_sensor/binary_sensor.cpp
@@ -30,7 +30,11 @@ void BinarySensor::publish_initial_state(bool state) {
   }
 }
 void BinarySensor::send_state_internal(bool state, bool is_initial) {
-  ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state ? "ON" : "OFF");
+  if (is_initial) {
+    ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
+  } else {
+    ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state));
+  }
   this->has_state_ = true;
   this->state = state;
   if (!is_initial) {
diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h
index 51c7a57ff6..f91c93c424 100644
--- a/esphome/components/binary_sensor/binary_sensor.h
+++ b/esphome/components/binary_sensor/binary_sensor.h
@@ -10,9 +10,9 @@ namespace binary_sensor {
 
 #define LOG_BINARY_SENSOR(prefix, type, obj) \
   if (obj != nullptr) { \
-    ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
     if (!obj->get_device_class().empty()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Device Class: '%s'", obj->get_device_class().c_str()); \
+      ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, obj->get_device_class().c_str()); \
     } \
   }
 
@@ -67,7 +67,7 @@ class BinarySensor : public Nameable {
   void send_state_internal(bool state, bool is_initial);
 
   /// Return whether this binary sensor has outputted a state.
-  bool has_state() const;
+  virtual bool has_state() const;
 
   virtual bool is_status_binary_sensor() const;
 
@@ -86,5 +86,10 @@ class BinarySensor : public Nameable {
   Deduplicator<bool> publish_dedup_;
 };
 
+class BinarySensorInitiallyOff : public BinarySensor {
+ public:
+  bool has_state() const override { return true; }
+};
+
 }  // namespace binary_sensor
 }  // namespace esphome
diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp
index b7ac2c4a79..f4612d62e9 100644
--- a/esphome/components/binary_sensor/filter.cpp
+++ b/esphome/components/binary_sensor/filter.cpp
@@ -23,6 +23,19 @@ void Filter::input(bool value, bool is_initial) {
     this->output(*b, is_initial);
   }
 }
+
+DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {}
+optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
+  if (value) {
+    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
+  } else {
+    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
+  }
+  return {};
+}
+
+float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
+
 DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
 optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
   if (value) {
@@ -46,6 +59,7 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
     return true;
   }
 }
+
 float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 
 optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h
index d1e9a0d23a..0b54251cda 100644
--- a/esphome/components/binary_sensor/filter.h
+++ b/esphome/components/binary_sensor/filter.h
@@ -25,6 +25,18 @@ class Filter {
   Deduplicator<bool> dedup_;
 };
 
+class DelayedOnOffFilter : public Filter, public Component {
+ public:
+  explicit DelayedOnOffFilter(uint32_t delay);
+
+  optional<bool> new_value(bool value, bool is_initial) override;
+
+  float get_setup_priority() const override;
+
+ protected:
+  uint32_t delay_;
+};
+
 class DelayedOnFilter : public Filter, public Component {
  public:
   explicit DelayedOnFilter(uint32_t delay);
diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py
index 8b8cd8fc4e..27f4654ded 100644
--- a/esphome/components/binary_sensor_map/sensor.py
+++ b/esphome/components/binary_sensor_map/sensor.py
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
 
 from esphome.components import sensor, binary_sensor
 from esphome.const import CONF_ID, CONF_CHANNELS, CONF_VALUE, CONF_TYPE, UNIT_EMPTY, \
-    ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR
+    ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP
 
 DEPENDENCIES = ['binary_sensor']
 
@@ -11,7 +11,6 @@ binary_sensor_map_ns = cg.esphome_ns.namespace('binary_sensor_map')
 BinarySensorMap = binary_sensor_map_ns.class_('BinarySensorMap', cg.Component, sensor.Sensor)
 SensorMapType = binary_sensor_map_ns.enum('SensorMapType')
 
-CONF_GROUP = 'group'
 SENSOR_MAP_TYPES = {
     CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP,
 }
diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h
index 262cc3eedf..4c3f3af1e0 100644
--- a/esphome/components/ble_presence/ble_presence_device.h
+++ b/esphome/components/ble_presence/ble_presence_device.h
@@ -9,7 +9,7 @@
 namespace esphome {
 namespace ble_presence {
 
-class BLEPresenceDevice : public binary_sensor::BinarySensor,
+class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
                           public esp32_ble_tracker::ESPBTDeviceListener,
                           public Component {
  public:
diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp
index f32a0d2861..b7c7f12f6f 100644
--- a/esphome/components/bme280/bme280.cpp
+++ b/esphome/components/bme280/bme280.cpp
@@ -178,7 +178,7 @@ void BME280Component::update() {
     return;
   }
 
-  float meas_time = 1;
+  float meas_time = 1.5;
   meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_);
   meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f;
   meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f;
diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py
new file mode 100644
index 0000000000..52885ae449
--- /dev/null
+++ b/esphome/components/captive_portal/__init__.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import web_server_base
+from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
+from esphome.const import CONF_ID
+from esphome.core import coroutine_with_priority
+
+AUTO_LOAD = ['web_server_base']
+DEPENDENCIES = ['wifi']
+
+captive_portal_ns = cg.esphome_ns.namespace('captive_portal')
+CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(CaptivePortal),
+    cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+@coroutine_with_priority(64.0)
+def to_code(config):
+    paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
+
+    var = cg.new_Pvariable(config[CONF_ID], paren)
+    yield cg.register_component(var, config)
+    cg.add_define('USE_CAPTIVE_PORTAL')
diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp
new file mode 100644
index 0000000000..83f85d354c
--- /dev/null
+++ b/esphome/components/captive_portal/captive_portal.cpp
@@ -0,0 +1,174 @@
+#include "captive_portal.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/components/wifi/wifi_component.h"
+
+namespace esphome {
+namespace captive_portal {
+
+static const char *TAG = "captive_portal";
+
+void CaptivePortal::handle_index(AsyncWebServerRequest *request) {
+  AsyncResponseStream *stream = request->beginResponseStream("text/html");
+  stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" "
+                  "content=\"width=device-width,initial-scale=1,user-scalable=no\"/><title>"));
+  stream->print(App.get_name().c_str());
+  stream->print(F("</title><link rel=\"stylesheet\" href=\"/stylesheet.css\">"));
+  stream->print(F("<script>function c(l){document.getElementById('ssid').value=l.innerText||l.textContent; "
+                  "document.getElementById('psk').focus();}</script>"));
+  stream->print(F("</head>"));
+  stream->print(F("<body><div class=\"main\"><h1>WiFi Networks</h1>"));
+
+  if (request->hasArg("save")) {
+    stream->print(F("<div class=\"info\">The ESP will now try to connect to the network...<br/>Please give it some "
+                    "time to connect.<br/>Note: Copy the changed network to your YAML file - the next OTA update will "
+                    "overwrite these settings.</div>"));
+  }
+
+  for (auto &scan : wifi::global_wifi_component->get_scan_result()) {
+    if (scan.get_is_hidden())
+      continue;
+
+    stream->print(F("<div class=\"network\" onclick=\"c(this)\"><a href=\"#\" class=\"network-left\">"));
+
+    if (scan.get_rssi() >= -50) {
+      stream->print(F("<img src=\"/wifi-strength-4.svg\">"));
+    } else if (scan.get_rssi() >= -65) {
+      stream->print(F("<img src=\"/wifi-strength-3.svg\">"));
+    } else if (scan.get_rssi() >= -85) {
+      stream->print(F("<img src=\"/wifi-strength-2.svg\">"));
+    } else {
+      stream->print(F("<img src=\"/wifi-strength-1.svg\">"));
+    }
+
+    stream->print(F("<span class=\"network-ssid\">"));
+    stream->print(scan.get_ssid().c_str());
+    stream->print(F("</span></a>"));
+    if (scan.get_with_auth()) {
+      stream->print(F("<img src=\"/lock.svg\">"));
+    }
+    stream->print(F("</div>"));
+  }
+
+  stream->print(F("<h3>WiFi Settings</h3><form method=\"GET\" action=\"/wifisave\"><input id=\"ssid\" name=\"ssid\" "
+                  "length=32 placeholder=\"SSID\"><br/><input id=\"psk\" name=\"psk\" length=64 type=\"password\" "
+                  "placeholder=\"Password\"><br/><br/><button type=\"submit\">Save</button></form><br><hr><br>"));
+  stream->print(F("<h1>OTA Update</h1><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
+                  "type=\"file\" name=\"update\"><button type=\"submit\">Update</button></form>"));
+  stream->print(F("</div></body></html>"));
+  request->send(stream);
+}
+void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
+  std::string ssid = request->arg("ssid").c_str();
+  std::string psk = request->arg("psk").c_str();
+  ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:");
+  ESP_LOGI(TAG, "  SSID='%s'", ssid.c_str());
+  ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str());
+  this->override_sta_(ssid, psk);
+  request->redirect("/?save=true");
+}
+void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) {
+  CaptivePortalSettings save{};
+  strcpy(save.ssid, ssid.c_str());
+  strcpy(save.password, password.c_str());
+  this->pref_.save(&save);
+
+  wifi::WiFiAP sta{};
+  sta.set_ssid(ssid);
+  sta.set_password(password);
+  wifi::global_wifi_component->set_sta(sta);
+}
+
+void CaptivePortal::setup() {
+  // Hash with compilation time
+  // This ensures the AP override is not applied for OTA
+  uint32_t hash = fnv1_hash(App.get_compilation_time());
+  this->pref_ = global_preferences.make_preference<CaptivePortalSettings>(hash, true);
+
+  CaptivePortalSettings save{};
+  if (this->pref_.load(&save)) {
+    this->override_sta_(save.ssid, save.password);
+  }
+}
+void CaptivePortal::start() {
+  this->base_->init();
+  if (!this->initialized_) {
+    this->base_->add_handler(this);
+    this->base_->add_ota_handler();
+  }
+
+  this->dns_server_ = new DNSServer();
+  this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
+  IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
+  this->dns_server_->start(53, "*", ip);
+
+  this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
+    bool not_found = false;
+    if (!this->active_) {
+      not_found = true;
+    } else if (req->host() == wifi::global_wifi_component->wifi_soft_ap_ip().toString()) {
+      not_found = true;
+    }
+
+    if (not_found) {
+      req->send(404, "text/html", "File not found");
+      return;
+    }
+
+    auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().toString();
+    req->redirect(url);
+  });
+
+  this->initialized_ = true;
+  this->active_ = true;
+}
+
+const char STYLESHEET_CSS[] PROGMEM =
+    R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})";
+const char LOCK_SVG[] PROGMEM =
+    R"(<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>)";
+
+void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
+  if (req->url() == "/") {
+    this->handle_index(req);
+    return;
+  } else if (req->url() == "/wifisave") {
+    this->handle_wifisave(req);
+    return;
+  } else if (req->url() == "/stylesheet.css") {
+    req->send_P(200, "text/css", STYLESHEET_CSS);
+    return;
+  } else if (req->url() == "/lock.svg") {
+    req->send_P(200, "image/svg+xml", LOCK_SVG);
+    return;
+  }
+
+  AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml");
+  stream->print(F("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M12 3A18.9 18.9 0 0 "
+                  "0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 "));
+  if (req->url() == "/wifi-strength-4.svg") {
+    stream->print(F("3z"));
+  } else {
+    if (req->url() == "/wifi-strength-1.svg") {
+      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4"));
+    } else if (req->url() == "/wifi-strength-2.svg") {
+      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4"));
+    } else if (req->url() == "/wifi-strength-3.svg") {
+      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2."));
+    }
+    stream->print(F("4A16.94 16.94 0 0 1 12 5z"));
+  }
+  stream->print(F("\"/></svg>"));
+  req->send(stream);
+}
+CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; }
+float CaptivePortal::get_setup_priority() const {
+  // Before WiFi
+  return setup_priority::WIFI + 1.0f;
+}
+void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); }
+
+CaptivePortal *global_captive_portal = nullptr;
+
+}  // namespace captive_portal
+}  // namespace esphome
diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h
new file mode 100644
index 0000000000..3af47546cf
--- /dev/null
+++ b/esphome/components/captive_portal/captive_portal.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <DNSServer.h>
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/preferences.h"
+#include "esphome/components/web_server_base/web_server_base.h"
+
+namespace esphome {
+
+namespace captive_portal {
+
+struct CaptivePortalSettings {
+  char ssid[33];
+  char password[65];
+} PACKED;  // NOLINT
+
+class CaptivePortal : public AsyncWebHandler, public Component {
+ public:
+  CaptivePortal(web_server_base::WebServerBase *base);
+  void setup() override;
+  void dump_config() override;
+  void loop() override {
+    if (this->dns_server_ != nullptr)
+      this->dns_server_->processNextRequest();
+  }
+  float get_setup_priority() const override;
+  void start();
+  bool is_active() const { return this->active_; }
+  void end() {
+    this->active_ = false;
+    this->base_->deinit();
+    this->dns_server_->stop();
+    delete this->dns_server_;
+  }
+
+  bool canHandle(AsyncWebServerRequest *request) override {
+    if (!this->active_)
+      return false;
+
+    if (request->method() == HTTP_GET) {
+      if (request->url() == "/")
+        return true;
+      if (request->url() == "/stylesheet.css")
+        return true;
+      if (request->url() == "/wifi-strength-1.svg")
+        return true;
+      if (request->url() == "/wifi-strength-2.svg")
+        return true;
+      if (request->url() == "/wifi-strength-3.svg")
+        return true;
+      if (request->url() == "/wifi-strength-4.svg")
+        return true;
+      if (request->url() == "/lock.svg")
+        return true;
+      if (request->url() == "/wifisave")
+        return true;
+    }
+
+    return false;
+  }
+
+  void handle_index(AsyncWebServerRequest *request);
+
+  void handle_wifisave(AsyncWebServerRequest *request);
+
+  void handleRequest(AsyncWebServerRequest *req) override;
+
+ protected:
+  void override_sta_(const std::string &ssid, const std::string &password);
+
+  web_server_base::WebServerBase *base_;
+  bool initialized_{false};
+  bool active_{false};
+  ESPPreferenceObject pref_;
+  DNSServer *dns_server_{nullptr};
+};
+
+extern CaptivePortal *global_captive_portal;
+
+}  // namespace captive_portal
+}  // namespace esphome
diff --git a/esphome/components/captive_portal/index.html b/esphome/components/captive_portal/index.html
new file mode 100644
index 0000000000..627bf81215
--- /dev/null
+++ b/esphome/components/captive_portal/index.html
@@ -0,0 +1,55 @@
+<!-- HTTP_HEAD -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
+    <title>{{ App.get_name() }}</title>
+    <link rel="stylesheet" href="./stylesheet.css">
+    <script>
+        function c(l) {
+            document.getElementById('ssid').value = l.innerText || l.textContent;
+            document.getElementById('psk').focus();
+        }
+    </script>
+</head>
+<body>
+<div class="main">
+    <h1>WiFi Networks</h1>
+    <div class="info">
+        The ESP will now try to connect to the network...<br/>
+        Please give it some time to connect.<br/>
+        Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings.
+    </div>
+    <div class="network" onclick="c(this)">
+        <a href="#" class="network-left">
+            <img src="./wifi-strength-4.svg">
+            <span class="network-ssid">AP1</span>
+        </a>
+        <img src="./lock.svg">
+    </div>
+    <div class="network" onclick="c(this)">
+        <a href="#" class="network-left">
+            <img src="./wifi-strength-2.svg">
+            <span class="network-ssid">AP2</span>
+        </a>
+    </div>
+
+    <h3>WiFi Settings</h3>
+    <form method="GET" action="/wifisave">
+        <input id="ssid" name="ssid" length=32 placeholder="SSID"><br/>
+        <input id="psk" name="psk" length=64 type="password" placeholder="Password"><br/>
+        <br/>
+        <button type="submit">Save</button>
+    </form>
+    <br><hr>
+    <br>
+
+    <h1>OTA Update</h1>
+    <form method="POST" action="/update" enctype="multipart/form-data">
+        <input type="file" name="update">
+        <button type="submit">Update</button>
+    </form>
+</div>
+</body>
+</html>
diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg
new file mode 100644
index 0000000000..743a1cc55a
--- /dev/null
+++ b/esphome/components/captive_portal/lock.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>
\ No newline at end of file
diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css
new file mode 100644
index 0000000000..73f82f05f1
--- /dev/null
+++ b/esphome/components/captive_portal/stylesheet.css
@@ -0,0 +1,58 @@
+* {
+    box-sizing: inherit;
+}
+
+div, input {
+    padding: 5px;
+    font-size: 1em;
+}
+
+input {
+    width: 95%;
+}
+
+body {
+    text-align: center;
+    font-family: sans-serif;
+}
+
+button {
+    border: 0;
+    border-radius: 0.3rem;
+    background-color: #1fa3ec;
+    color: #fff;
+    line-height: 2.4rem;
+    font-size: 1.2rem;
+    width: 100%;
+    padding: 0;
+}
+
+.main {
+    text-align: left;
+    display: inline-block;
+    min-width: 260px;
+}
+
+.network {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.network-left {
+    display: flex;
+    align-items: center;
+}
+
+.network-ssid {
+    margin-bottom: -7px;
+    margin-left: 10px;
+}
+
+.info {
+    border: 1px solid;
+    margin: 10px 0px;
+    padding: 15px 10px;
+    color: #4f8a10;
+    background-color: #dff2bf;
+}
diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg
new file mode 100644
index 0000000000..189a38193c
--- /dev/null
+++ b/esphome/components/captive_portal/wifi-strength-1.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg>
\ No newline at end of file
diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg
new file mode 100644
index 0000000000..9b4b2d2396
--- /dev/null
+++ b/esphome/components/captive_portal/wifi-strength-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg>
\ No newline at end of file
diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg
new file mode 100644
index 0000000000..44b7532bb7
--- /dev/null
+++ b/esphome/components/captive_portal/wifi-strength-3.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.4A16.94 16.94 0 0 1 12 5z"/></svg>
\ No newline at end of file
diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg
new file mode 100644
index 0000000000..a22b0b8281
--- /dev/null
+++ b/esphome/components/captive_portal/wifi-strength-4.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3z"/></svg>
\ No newline at end of file
diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp
index 2b40f932f9..7c7da6bb0c 100644
--- a/esphome/components/climate/climate.cpp
+++ b/esphome/components/climate/climate.cpp
@@ -173,6 +173,9 @@ void Climate::publish_state() {
   auto traits = this->get_traits();
 
   ESP_LOGD(TAG, "  Mode: %s", climate_mode_to_string(this->mode));
+  if (traits.get_supports_action()) {
+    ESP_LOGD(TAG, "  Action: %s", climate_action_to_string(this->action));
+  }
   if (traits.get_supports_current_temperature()) {
     ESP_LOGD(TAG, "  Current Temperature: %.2f°C", this->current_temperature);
   }
diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h
index c58eed1a7c..4dd872bbed 100644
--- a/esphome/components/climate/climate.h
+++ b/esphome/components/climate/climate.h
@@ -9,6 +9,11 @@
 namespace esphome {
 namespace climate {
 
+#define LOG_CLIMATE(prefix, type, obj) \
+  if (obj != nullptr) { \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
+  }
+
 class Climate;
 
 /** This class is used to encode all control actions on a climate device.
@@ -121,6 +126,8 @@ class Climate : public Nameable {
 
   /// The active mode of the climate device.
   ClimateMode mode{CLIMATE_MODE_OFF};
+  /// The active state of the climate device.
+  ClimateAction action{CLIMATE_ACTION_OFF};
   /// The current temperature of the climate device, as reported from the integration.
   float current_temperature{NAN};
 
diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp
index 07b97f4f33..34aa564fb0 100644
--- a/esphome/components/climate/climate_mode.cpp
+++ b/esphome/components/climate/climate_mode.cpp
@@ -17,6 +17,18 @@ const char *climate_mode_to_string(ClimateMode mode) {
       return "UNKNOWN";
   }
 }
+const char *climate_action_to_string(ClimateAction action) {
+  switch (action) {
+    case CLIMATE_ACTION_OFF:
+      return "OFF";
+    case CLIMATE_ACTION_COOLING:
+      return "COOLING";
+    case CLIMATE_ACTION_HEATING:
+      return "HEATING";
+    default:
+      return "UNKNOWN";
+  }
+}
 
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h
index 28608b7cd8..e5786286d8 100644
--- a/esphome/components/climate/climate_mode.h
+++ b/esphome/components/climate/climate_mode.h
@@ -17,8 +17,19 @@ enum ClimateMode : uint8_t {
   CLIMATE_MODE_HEAT = 3,
 };
 
+/// Enum for the current action of the climate device. Values match those of ClimateMode.
+enum ClimateAction : uint8_t {
+  /// The climate device is off (inactive or no power)
+  CLIMATE_ACTION_OFF = 0,
+  /// The climate device is actively cooling (usually in cool or auto mode)
+  CLIMATE_ACTION_COOLING = 2,
+  /// The climate device is actively heating (usually in heat or auto mode)
+  CLIMATE_ACTION_HEATING = 3,
+};
+
 /// Convert the given ClimateMode to a human-readable string.
 const char *climate_mode_to_string(ClimateMode mode);
+const char *climate_action_to_string(ClimateAction action);
 
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp
index 712186aa80..a1db2bc696 100644
--- a/esphome/components/climate/climate_traits.cpp
+++ b/esphome/components/climate/climate_traits.cpp
@@ -30,6 +30,7 @@ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_a
 void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; }
 void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; }
 void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; }
+void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; }
 float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; }
 void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) {
   visual_min_temperature_ = visual_min_temperature;
@@ -52,6 +53,7 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const {
 }
 void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; }
 bool ClimateTraits::get_supports_away() const { return supports_away_; }
+bool ClimateTraits::get_supports_action() const { return supports_action_; }
 
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h
index 34e03455b1..2d6f44eea6 100644
--- a/esphome/components/climate/climate_traits.h
+++ b/esphome/components/climate/climate_traits.h
@@ -23,6 +23,8 @@ namespace climate {
  *    - heat mode (increases current temperature)
  *  - supports away - away mode means that the climate device supports two different
  *      target temperature settings: one target temp setting for "away" mode and one for non-away mode.
+ *  - supports action - if the climate device supports reporting the active
+ *    current action of the device with the action property.
  *
  * This class also contains static data for the climate device display:
  *  - visual min/max temperature - tells the frontend what range of temperatures the climate device
@@ -41,6 +43,8 @@ class ClimateTraits {
   void set_supports_heat_mode(bool supports_heat_mode);
   void set_supports_away(bool supports_away);
   bool get_supports_away() const;
+  void set_supports_action(bool supports_action);
+  bool get_supports_action() const;
   bool supports_mode(ClimateMode mode) const;
 
   float get_visual_min_temperature() const;
@@ -58,6 +62,7 @@ class ClimateTraits {
   bool supports_cool_mode_{false};
   bool supports_heat_mode_{false};
   bool supports_away_{false};
+  bool supports_action_{false};
 
   float visual_min_temperature_{10};
   float visual_max_temperature_{30};
diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py
new file mode 100644
index 0000000000..1163705faa
--- /dev/null
+++ b/esphome/components/climate_ir/__init__.py
@@ -0,0 +1,41 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import climate, remote_transmitter, remote_receiver, sensor, remote_base
+from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID
+from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR
+from esphome.core import coroutine
+
+AUTO_LOAD = ['sensor', 'remote_base']
+
+climate_ir_ns = cg.esphome_ns.namespace('climate_ir')
+ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component,
+                                 remote_base.RemoteReceiverListener)
+
+CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend({
+    cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
+    cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
+    cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
+    cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
+}).extend(cv.COMPONENT_SCHEMA)
+
+CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend({
+    cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent),
+})
+
+
+@coroutine
+def register_climate_ir(var, config):
+    yield cg.register_component(var, config)
+    yield climate.register_climate(var, config)
+
+    cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
+    cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
+    if CONF_SENSOR in config:
+        sens = yield cg.get_variable(config[CONF_SENSOR])
+        cg.add(var.set_sensor(sens))
+    if CONF_RECEIVER_ID in config:
+        receiver = yield cg.get_variable(config[CONF_RECEIVER_ID])
+        cg.add(receiver.register_listener(var))
+
+    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
+    cg.add(var.set_transmitter(transmitter))
diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp
new file mode 100644
index 0000000000..4b9a1c0baa
--- /dev/null
+++ b/esphome/components/climate_ir/climate_ir.cpp
@@ -0,0 +1,67 @@
+#include "climate_ir.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace climate_ir {
+
+static const char *TAG = "climate_ir";
+
+climate::ClimateTraits ClimateIR::traits() {
+  auto traits = climate::ClimateTraits();
+  traits.set_supports_current_temperature(this->sensor_ != nullptr);
+  traits.set_supports_auto_mode(true);
+  traits.set_supports_cool_mode(this->supports_cool_);
+  traits.set_supports_heat_mode(this->supports_heat_);
+  traits.set_supports_two_point_target_temperature(false);
+  traits.set_supports_away(false);
+  traits.set_visual_min_temperature(this->minimum_temperature_);
+  traits.set_visual_max_temperature(this->maximum_temperature_);
+  traits.set_visual_temperature_step(this->temperature_step_);
+  return traits;
+}
+
+void ClimateIR::setup() {
+  if (this->sensor_) {
+    this->sensor_->add_on_state_callback([this](float state) {
+      this->current_temperature = state;
+      // current temperature changed, publish state
+      this->publish_state();
+    });
+    this->current_temperature = this->sensor_->state;
+  } else
+    this->current_temperature = NAN;
+  // restore set points
+  auto restore = this->restore_state_();
+  if (restore.has_value()) {
+    restore->apply(this);
+  } else {
+    // restore from defaults
+    this->mode = climate::CLIMATE_MODE_OFF;
+    // initialize target temperature to some value so that it's not NAN
+    this->target_temperature =
+        roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_));
+  }
+  // Never send nan to HA
+  if (isnan(this->target_temperature))
+    this->target_temperature = 24;
+}
+
+void ClimateIR::control(const climate::ClimateCall &call) {
+  if (call.get_mode().has_value())
+    this->mode = *call.get_mode();
+  if (call.get_target_temperature().has_value())
+    this->target_temperature = *call.get_target_temperature();
+
+  this->transmit_state();
+  this->publish_state();
+}
+void ClimateIR::dump_config() {
+  LOG_CLIMATE("", "IR Climate", this);
+  ESP_LOGCONFIG(TAG, "  Min. Temperature: %.1f°C", this->minimum_temperature_);
+  ESP_LOGCONFIG(TAG, "  Max. Temperature: %.1f°C", this->maximum_temperature_);
+  ESP_LOGCONFIG(TAG, "  Supports HEAT: %s", YESNO(this->supports_heat_));
+  ESP_LOGCONFIG(TAG, "  Supports COOL: %s", YESNO(this->supports_cool_));
+}
+
+}  // namespace climate_ir
+}  // namespace esphome
diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h
new file mode 100644
index 0000000000..b4c036f3d6
--- /dev/null
+++ b/esphome/components/climate_ir/climate_ir.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "esphome/components/climate/climate.h"
+#include "esphome/components/remote_base/remote_base.h"
+#include "esphome/components/remote_transmitter/remote_transmitter.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace climate_ir {
+
+/* A base for climate which works by sending (and receiving) IR codes
+
+    To send IR codes implement
+      void ClimateIR::transmit_state_()
+
+    Likewise to decode a IR into the AC state, implement
+      bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true
+*/
+class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener {
+ public:
+  ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) {
+    this->minimum_temperature_ = minimum_temperature;
+    this->maximum_temperature_ = maximum_temperature;
+    this->temperature_step_ = temperature_step;
+  }
+
+  void setup() override;
+  void dump_config() override;
+  void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
+    this->transmitter_ = transmitter;
+  }
+  void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
+  void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
+  void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
+
+ protected:
+  float minimum_temperature_, maximum_temperature_, temperature_step_;
+
+  /// 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;
+
+  /// Transmit via IR the state of this climate controller.
+  virtual void transmit_state() = 0;
+
+  bool supports_cool_{true};
+  bool supports_heat_{true};
+
+  remote_transmitter::RemoteTransmitterComponent *transmitter_;
+  sensor::Sensor *sensor_{nullptr};
+};
+
+}  // namespace climate_ir
+}  // namespace esphome
diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py
index 750a97d087..81412bb586 100644
--- a/esphome/components/coolix/climate.py
+++ b/esphome/components/coolix/climate.py
@@ -1,36 +1,18 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import climate, remote_transmitter, sensor
-from esphome.const import CONF_ID, CONF_SENSOR
+from esphome.components import climate_ir
+from esphome.const import CONF_ID
 
-AUTO_LOAD = ['sensor']
+AUTO_LOAD = ['climate_ir']
 
 coolix_ns = cg.esphome_ns.namespace('coolix')
-CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component)
+CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR)
 
-CONF_TRANSMITTER_ID = 'transmitter_id'
-CONF_SUPPORTS_HEAT = 'supports_heat'
-CONF_SUPPORTS_COOL = 'supports_cool'
-
-CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
+CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(CoolixClimate),
-    cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
-    cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
-    cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
-    cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
-}).extend(cv.COMPONENT_SCHEMA))
+})
 
 
 def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
-
-    cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
-    cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
-    if CONF_SENSOR in config:
-        sens = yield cg.get_variable(config[CONF_SENSOR])
-        cg.add(var.set_sensor(sens))
-
-    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
-    cg.add(var.set_transmitter(transmitter))
+    yield climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp
index ffc67adeb3..c08571c2e9 100644
--- a/esphome/components/coolix/coolix.cpp
+++ b/esphome/components/coolix/coolix.cpp
@@ -7,19 +7,26 @@ namespace coolix {
 static const char *TAG = "coolix.climate";
 
 const uint32_t COOLIX_OFF = 0xB27BE0;
+const uint32_t COOLIX_SWING = 0xB26BE0;
+const uint32_t COOLIX_LED = 0xB5F5A5;
+const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6;
+
 // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore.
 const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8;
 const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48;
-const uint8_t COOLIX_COOL = 0b00;
-const uint8_t COOLIX_DRY = 0b01;
-const uint8_t COOLIX_AUTO = 0b10;
-const uint8_t COOLIX_HEAT = 0b11;
-const uint8_t COOLIX_FAN = 4;                                  // Synthetic.
-const uint32_t COOLIX_MODE_MASK = 0b000000000000000000001100;  // 0xC
+const uint8_t COOLIX_COOL = 0b0000;
+const uint8_t COOLIX_DRY_FAN = 0b0100;
+const uint8_t COOLIX_AUTO = 0b1000;
+const uint8_t COOLIX_HEAT = 0b1100;
+const uint32_t COOLIX_MODE_MASK = 0b1100;
+const uint32_t COOLIX_FAN_MASK = 0xF000;
+const uint32_t COOLIX_FAN_DRY = 0x1000;
+const uint32_t COOLIX_FAN_AUTO = 0xB000;
+const uint32_t COOLIX_FAN_MIN = 0x9000;
+const uint32_t COOLIX_FAN_MED = 0x5000;
+const uint32_t COOLIX_FAN_MAX = 0x3000;
 
 // Temperature
-const uint8_t COOLIX_TEMP_MIN = 17;  // Celsius
-const uint8_t COOLIX_TEMP_MAX = 30;  // Celsius
 const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1;
 const uint8_t COOLIX_FAN_TEMP_CODE = 0b1110;  // Part of Fan Mode.
 const uint32_t COOLIX_TEMP_MASK = 0b11110000;
@@ -41,80 +48,25 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = {
 };
 
 // Constants
-// Pulse parms are *50-100 for the Mark and *50+100 for the space
-// First MARK is the one after the long gap
-// pulse parameters in usec
-const uint16_t COOLIX_TICK = 560;  // Approximately 21 cycles at 38kHz
-const uint16_t COOLIX_BIT_MARK_TICKS = 1;
-const uint16_t COOLIX_BIT_MARK = COOLIX_BIT_MARK_TICKS * COOLIX_TICK;
-const uint16_t COOLIX_ONE_SPACE_TICKS = 3;
-const uint16_t COOLIX_ONE_SPACE = COOLIX_ONE_SPACE_TICKS * COOLIX_TICK;
-const uint16_t COOLIX_ZERO_SPACE_TICKS = 1;
-const uint16_t COOLIX_ZERO_SPACE = COOLIX_ZERO_SPACE_TICKS * COOLIX_TICK;
-const uint16_t COOLIX_HEADER_MARK_TICKS = 8;
-const uint16_t COOLIX_HEADER_MARK = COOLIX_HEADER_MARK_TICKS * COOLIX_TICK;
-const uint16_t COOLIX_HEADER_SPACE_TICKS = 8;
-const uint16_t COOLIX_HEADER_SPACE = COOLIX_HEADER_SPACE_TICKS * COOLIX_TICK;
-const uint16_t COOLIX_MIN_GAP_TICKS = COOLIX_HEADER_MARK_TICKS + COOLIX_ZERO_SPACE_TICKS;
-const uint16_t COOLIX_MIN_GAP = COOLIX_MIN_GAP_TICKS * COOLIX_TICK;
+static const uint32_t BIT_MARK_US = 660;
+static const uint32_t HEADER_MARK_US = 560 * 8;
+static const uint32_t HEADER_SPACE_US = 560 * 8;
+static const uint32_t BIT_ONE_SPACE_US = 1500;
+static const uint32_t BIT_ZERO_SPACE_US = 450;
+static const uint32_t FOOTER_MARK_US = BIT_MARK_US;
+static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US;
 
 const uint16_t COOLIX_BITS = 24;
 
-climate::ClimateTraits CoolixClimate::traits() {
-  auto traits = climate::ClimateTraits();
-  traits.set_supports_current_temperature(this->sensor_ != nullptr);
-  traits.set_supports_auto_mode(true);
-  traits.set_supports_cool_mode(this->supports_cool_);
-  traits.set_supports_heat_mode(this->supports_heat_);
-  traits.set_supports_two_point_target_temperature(false);
-  traits.set_supports_away(false);
-  traits.set_visual_min_temperature(17);
-  traits.set_visual_max_temperature(30);
-  traits.set_visual_temperature_step(1);
-  return traits;
-}
-
-void CoolixClimate::setup() {
-  if (this->sensor_) {
-    this->sensor_->add_on_state_callback([this](float state) {
-      this->current_temperature = state;
-      // current temperature changed, publish state
-      this->publish_state();
-    });
-    this->current_temperature = this->sensor_->state;
-  } else
-    this->current_temperature = NAN;
-  // restore set points
-  auto restore = this->restore_state_();
-  if (restore.has_value()) {
-    restore->apply(this);
-  } else {
-    // restore from defaults
-    this->mode = climate::CLIMATE_MODE_AUTO;
-    // initialize target temperature to some value so that it's not NAN
-    this->target_temperature = roundf(this->current_temperature);
-  }
-}
-
-void CoolixClimate::control(const climate::ClimateCall &call) {
-  if (call.get_mode().has_value())
-    this->mode = *call.get_mode();
-  if (call.get_target_temperature().has_value())
-    this->target_temperature = *call.get_target_temperature();
-
-  this->transmit_state_();
-  this->publish_state();
-}
-
-void CoolixClimate::transmit_state_() {
+void CoolixClimate::transmit_state() {
   uint32_t remote_state;
 
   switch (this->mode) {
     case climate::CLIMATE_MODE_COOL:
-      remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_COOL << 2);
+      remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL;
       break;
     case climate::CLIMATE_MODE_HEAT:
-      remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_HEAT << 2);
+      remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT;
       break;
     case climate::CLIMATE_MODE_AUTO:
       remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN;
@@ -127,10 +79,10 @@ void CoolixClimate::transmit_state_() {
   if (this->mode != climate::CLIMATE_MODE_OFF) {
     auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX));
     remote_state &= ~COOLIX_TEMP_MASK;  // Clear the old temp.
-    remote_state |= (COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4);
+    remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4;
   }
 
-  ESP_LOGV(TAG, "Sending coolix code: %u", remote_state);
+  ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state);
 
   auto transmit = this->transmitter_->transmit();
   auto data = transmit.get_data();
@@ -139,32 +91,113 @@ void CoolixClimate::transmit_state_() {
   uint16_t repeat = 1;
   for (uint16_t r = 0; r <= repeat; r++) {
     // Header
-    data->mark(COOLIX_HEADER_MARK);
-    data->space(COOLIX_HEADER_SPACE);
+    data->mark(HEADER_MARK_US);
+    data->space(HEADER_SPACE_US);
     // Data
-    //   Break data into byte segments, starting at the Most Significant
+    //   Break data into bytes, starting at the Most Significant
     //   Byte. Each byte then being sent normal, then followed inverted.
     for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) {
       // Grab a bytes worth of data.
-      uint8_t segment = (remote_state >> (COOLIX_BITS - i)) & 0xFF;
+      uint8_t byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF;
       // Normal
       for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) {
-        data->mark(COOLIX_BIT_MARK);
-        data->space((segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE);
+        data->mark(BIT_MARK_US);
+        data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US);
       }
       // Inverted
       for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) {
-        data->mark(COOLIX_BIT_MARK);
-        data->space(!(segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE);
+        data->mark(BIT_MARK_US);
+        data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US);
       }
     }
     // Footer
-    data->mark(COOLIX_BIT_MARK);
-    data->space(COOLIX_MIN_GAP);  // Pause before repeating
+    data->mark(BIT_MARK_US);
+    data->space(FOOTER_SPACE_US);  // Pause before repeating
   }
 
   transmit.perform();
 }
 
+bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) {
+  // Decoded remote state y 3 bytes long code.
+  uint32_t remote_state = 0;
+  // The protocol sends the data twice, read here
+  uint32_t loop_read;
+  for (uint16_t loop = 1; loop <= 2; loop++) {
+    if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US))
+      return false;
+    loop_read = 0;
+    for (uint8_t a_byte = 0; a_byte < 3; a_byte++) {
+      uint8_t byte = 0;
+      for (int8_t a_bit = 7; a_bit >= 0; a_bit--) {
+        if (data.expect_item(BIT_MARK_US, BIT_ONE_SPACE_US))
+          byte |= 1 << a_bit;
+        else if (!data.expect_item(BIT_MARK_US, BIT_ZERO_SPACE_US))
+          return false;
+      }
+      // Need to see this segment inverted
+      for (int8_t a_bit = 7; a_bit >= 0; a_bit--) {
+        bool bit = byte & (1 << a_bit);
+        if (!data.expect_item(BIT_MARK_US, bit ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US))
+          return false;
+      }
+      // Receiving MSB first: reorder bytes
+      loop_read |= byte << ((2 - a_byte) * 8);
+    }
+    // Footer Mark
+    if (!data.expect_mark(BIT_MARK_US))
+      return false;
+    if (loop == 1) {
+      // Back up state on first loop
+      remote_state = loop_read;
+      if (!data.expect_space(FOOTER_SPACE_US))
+        return false;
+    }
+  }
+
+  ESP_LOGV(TAG, "Decoded 0x%02X", remote_state);
+  if (remote_state != loop_read || (remote_state & 0xFF0000) != 0xB20000)
+    return false;
+
+  if (remote_state == COOLIX_OFF) {
+    this->mode = climate::CLIMATE_MODE_OFF;
+  } else {
+    if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT)
+      this->mode = climate::CLIMATE_MODE_HEAT;
+    else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO)
+      this->mode = climate::CLIMATE_MODE_AUTO;
+    else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) {
+      // climate::CLIMATE_MODE_DRY;
+      if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY)
+        ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO");
+      else
+        ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO");
+      this->mode = climate::CLIMATE_MODE_AUTO;
+    } else
+      this->mode = climate::CLIMATE_MODE_COOL;
+
+    // Fan Speed
+    // When climate::CLIMATE_MODE_DRY is implemented replace following line with this:
+    //   if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY)
+    if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO)
+      ESP_LOGV(TAG, "Not supported FAN speed AUTO");
+    else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN)
+      ESP_LOGV(TAG, "Not supported FAN speed MIN");
+    else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED)
+      ESP_LOGV(TAG, "Not supported FAN speed MED");
+    else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX)
+      ESP_LOGV(TAG, "Not supported FAN speed MAX");
+
+    // Temperature
+    uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4;
+    for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++)
+      if (COOLIX_TEMP_MAP[i] == temperature_code)
+        this->target_temperature = i + COOLIX_TEMP_MIN;
+  }
+  this->publish_state();
+
+  return true;
+}
+
 }  // namespace coolix
 }  // namespace esphome
diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h
index 0d52018d2a..ed03a2fd1e 100644
--- a/esphome/components/coolix/coolix.h
+++ b/esphome/components/coolix/coolix.h
@@ -1,39 +1,23 @@
 #pragma once
 
-#include "esphome/core/component.h"
-#include "esphome/core/automation.h"
-#include "esphome/components/climate/climate.h"
-#include "esphome/components/remote_base/remote_base.h"
-#include "esphome/components/remote_transmitter/remote_transmitter.h"
-#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/climate_ir/climate_ir.h"
 
 namespace esphome {
 namespace coolix {
 
-class CoolixClimate : public climate::Climate, public Component {
+// Temperature
+const uint8_t COOLIX_TEMP_MIN = 17;  // Celsius
+const uint8_t COOLIX_TEMP_MAX = 30;  // Celsius
+
+class CoolixClimate : public climate_ir::ClimateIR {
  public:
-  void setup() override;
-  void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
-    this->transmitter_ = transmitter;
-  }
-  void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
-  void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
-  void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
+  CoolixClimate() : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {}
 
  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;
-
   /// Transmit via IR the state of this climate controller.
-  void transmit_state_();
-
-  bool supports_cool_{true};
-  bool supports_heat_{true};
-
-  remote_transmitter::RemoteTransmitterComponent *transmitter_;
-  sensor::Sensor *sensor_{nullptr};
+  void transmit_state() override;
+  /// Handle received IR Buffer
+  bool on_receive(remote_base::RemoteReceiveData data) override;
 };
 
 }  // namespace coolix
diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h
index 12011e1b4c..839cf9207e 100644
--- a/esphome/components/cover/cover.h
+++ b/esphome/components/cover/cover.h
@@ -13,13 +13,13 @@ const extern float COVER_CLOSED;
 
 #define LOG_COVER(prefix, type, obj) \
   if (obj != nullptr) { \
-    ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
     auto traits_ = obj->get_traits(); \
     if (traits_.get_is_assumed_state()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Assumed State: YES"); \
+      ESP_LOGCONFIG(TAG, "%s  Assumed State: YES", prefix); \
     } \
     if (!obj->get_device_class().empty()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Device Class: '%s'", obj->get_device_class().c_str()); \
+      ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, obj->get_device_class().c_str()); \
     } \
   }
 
diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp
index 358453a63a..6c014138fd 100644
--- a/esphome/components/cse7766/cse7766.cpp
+++ b/esphome/components/cse7766/cse7766.cpp
@@ -172,6 +172,7 @@ void CSE7766Component::dump_config() {
   LOG_SENSOR("  ", "Voltage", this->voltage_sensor_);
   LOG_SENSOR("  ", "Current", this->current_sensor_);
   LOG_SENSOR("  ", "Power", this->power_sensor_);
+  this->check_uart_settings(4800);
 }
 
 }  // namespace cse7766
diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp
index 1a7bac2844..674cc0ae98 100644
--- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp
+++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp
@@ -8,6 +8,18 @@ namespace ct_clamp {
 
 static const char *TAG = "ct_clamp";
 
+void CTClampSensor::setup() {
+  this->is_calibrating_offset_ = true;
+  this->high_freq_.start();
+  this->set_timeout("calibrate_offset", this->sample_duration_, [this]() {
+    this->high_freq_.stop();
+    this->is_calibrating_offset_ = false;
+    if (this->num_samples_ != 0) {
+      this->offset_ = this->sample_sum_ / this->num_samples_;
+    }
+  });
+}
+
 void CTClampSensor::dump_config() {
   LOG_SENSOR("", "CT Clamp Sensor", this);
   ESP_LOGCONFIG(TAG, "  Sample Duration: %.2fs", this->sample_duration_ / 1e3f);
@@ -15,6 +27,9 @@ void CTClampSensor::dump_config() {
 }
 
 void CTClampSensor::update() {
+  if (this->is_calibrating_offset_)
+    return;
+
   // Update only starts the sampling phase, in loop() the actual sampling is happening.
 
   // Request a high loop() execution interval during sampling phase.
@@ -44,12 +59,18 @@ void CTClampSensor::update() {
 }
 
 void CTClampSensor::loop() {
-  if (!this->is_sampling_)
+  if (!this->is_sampling_ && !this->is_calibrating_offset_)
     return;
 
   // Perform a single sample
   float value = this->source_->sample();
 
+  if (this->is_calibrating_offset_) {
+    this->sample_sum_ += value;
+    this->num_samples_++;
+    return;
+  }
+
   // Adjust DC offset via low pass filter (exponential moving average)
   const float alpha = 0.001f;
   this->offset_ = this->offset_ * (1 - alpha) + value * alpha;
diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.h b/esphome/components/ct_clamp/ct_clamp_sensor.h
index d816ac781a..c709f6718b 100644
--- a/esphome/components/ct_clamp/ct_clamp_sensor.h
+++ b/esphome/components/ct_clamp/ct_clamp_sensor.h
@@ -10,10 +10,14 @@ namespace ct_clamp {
 
 class CTClampSensor : public sensor::Sensor, public PollingComponent {
  public:
+  void setup() override;
   void update() override;
   void loop() override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
+  float get_setup_priority() const override {
+    // After the base sensor has been initialized
+    return setup_priority::DATA - 1.0f;
+  }
 
   void set_sample_duration(uint32_t sample_duration) { sample_duration_ = sample_duration; }
   void set_source(voltage_sampler::VoltageSampler *source) { source_ = source; }
@@ -40,6 +44,8 @@ class CTClampSensor : public sensor::Sensor, public PollingComponent {
   float sample_sum_ = 0.0f;
   uint32_t num_samples_ = 0;
   bool is_sampling_ = false;
+  /// Calibrate offset value once at boot
+  bool is_calibrating_offset_ = false;
 };
 
 }  // namespace ct_clamp
diff --git a/esphome/components/custom/output/__init__.py b/esphome/components/custom/output/__init__.py
index 6042863872..efe6f19dab 100644
--- a/esphome/components/custom/output/__init__.py
+++ b/esphome/components/custom/output/__init__.py
@@ -1,13 +1,12 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import output
-from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE
+from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE, CONF_BINARY
 from .. import custom_ns
 
 CustomBinaryOutputConstructor = custom_ns.class_('CustomBinaryOutputConstructor')
 CustomFloatOutputConstructor = custom_ns.class_('CustomFloatOutputConstructor')
 
-CONF_BINARY = 'binary'
 CONF_FLOAT = 'float'
 
 CONFIG_SCHEMA = cv.typed_schema({
diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp
index 1d3e693ff9..6eeddb1b56 100644
--- a/esphome/components/dallas/dallas_component.cpp
+++ b/esphome/components/dallas/dallas_component.cpp
@@ -132,10 +132,14 @@ void DallasComponent::update() {
       enable_interrupts();
 
       if (!res) {
+        ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str());
+        sensor->publish_state(NAN);
         this->status_set_warning();
         return;
       }
       if (!sensor->check_scratch_pad()) {
+        ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str());
+        sensor->publish_state(NAN);
         this->status_set_warning();
         return;
       }
@@ -244,11 +248,7 @@ bool DallasTemperatureSensor::check_scratch_pad() {
             this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
             crc8(this->scratch_pad_, 8));
 #endif
-  if (crc8(this->scratch_pad_, 8) != this->scratch_pad_[8]) {
-    ESP_LOGE(TAG, "Reading scratchpad from Dallas Sensor failed");
-    return false;
-  }
-  return true;
+  return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8];
 }
 float DallasTemperatureSensor::get_temp_c() {
   int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3);
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index 93ef04d195..5babf422bd 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -1,7 +1,6 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins, automation
-from esphome.automation import maybe_simple_id
 from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_CYCLES, \
     CONF_RUN_DURATION, CONF_SLEEP_DURATION, CONF_WAKEUP_PIN
 
@@ -85,7 +84,7 @@ def to_code(config):
     cg.add_define('USE_DEEP_SLEEP')
 
 
-DEEP_SLEEP_ACTION_SCHEMA = maybe_simple_id({
+DEEP_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id({
     cv.GenerateID(): cv.use_id(DeepSleepComponent),
 })
 
diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py
new file mode 100644
index 0000000000..890c2bede4
--- /dev/null
+++ b/esphome/components/dfplayer/__init__.py
@@ -0,0 +1,219 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE
+from esphome.components import uart
+
+DEPENDENCIES = ['uart']
+
+dfplayer_ns = cg.esphome_ns.namespace('dfplayer')
+DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component)
+DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_('DFPlayerFinishedPlaybackTrigger',
+                                                     automation.Trigger.template())
+DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', automation.Condition)
+
+MULTI_CONF = True
+CONF_FOLDER = 'folder'
+CONF_LOOP = 'loop'
+CONF_VOLUME = 'volume'
+CONF_EQ_PRESET = 'eq_preset'
+CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback'
+
+EqPreset = dfplayer_ns.enum("EqPreset")
+EQ_PRESET = {
+    'NORMAL': EqPreset.NORMAL,
+    'POP': EqPreset.POP,
+    'ROCK': EqPreset.ROCK,
+    'JAZZ': EqPreset.JAZZ,
+    'CLASSIC': EqPreset.CLASSIC,
+    'BASS': EqPreset.BASS,
+}
+Device = dfplayer_ns.enum("Device")
+DEVICE = {
+    'USB': Device.USB,
+    'TF_CARD': Device.TF_CARD,
+}
+
+NextAction = dfplayer_ns.class_('NextAction', automation.Action)
+PreviousAction = dfplayer_ns.class_('PreviousAction', automation.Action)
+PlayFileAction = dfplayer_ns.class_('PlayFileAction', automation.Action)
+PlayFolderAction = dfplayer_ns.class_('PlayFolderAction', automation.Action)
+SetVolumeAction = dfplayer_ns.class_('SetVolumeAction', automation.Action)
+SetEqAction = dfplayer_ns.class_('SetEqAction', automation.Action)
+SleepAction = dfplayer_ns.class_('SleepAction', automation.Action)
+ResetAction = dfplayer_ns.class_('ResetAction', automation.Action)
+StartAction = dfplayer_ns.class_('StartAction', automation.Action)
+PauseAction = dfplayer_ns.class_('PauseAction', automation.Action)
+StopAction = dfplayer_ns.class_('StopAction', automation.Action)
+RandomAction = dfplayer_ns.class_('RandomAction', automation.Action)
+SetDeviceAction = dfplayer_ns.class_('SetDeviceAction', automation.Action)
+
+CONFIG_SCHEMA = cv.All(cv.Schema({
+    cv.GenerateID(): cv.declare_id(DFPlayer),
+    cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({
+        cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DFPlayerFinishedPlaybackTrigger),
+    }),
+}).extend(uart.UART_DEVICE_SCHEMA))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield uart.register_uart_device(var, config)
+
+    for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        yield automation.build_automation(trigger, [], conf)
+
+
+@automation.register_action('dfplayer.play_next', NextAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_next_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.play_previous', PreviousAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_previous_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.play', PlayFileAction, cv.maybe_simple_value({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+    cv.Required(CONF_FILE): cv.templatable(cv.int_),
+    cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
+}, key=CONF_FILE))
+def dfplayer_play_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    template_ = yield cg.templatable(config[CONF_FILE], args, float)
+    cg.add(var.set_file(template_))
+    if CONF_LOOP in config:
+        template_ = yield cg.templatable(config[CONF_LOOP], args, float)
+        cg.add(var.set_loop(template_))
+    yield var
+
+
+@automation.register_action('dfplayer.play_folder', PlayFolderAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+    cv.Required(CONF_FOLDER): cv.templatable(cv.int_),
+    cv.Optional(CONF_FILE): cv.templatable(cv.int_),
+    cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
+}))
+def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    template_ = yield cg.templatable(config[CONF_FOLDER], args, float)
+    cg.add(var.set_folder(template_))
+    if CONF_FILE in config:
+        template_ = yield cg.templatable(config[CONF_FILE], args, float)
+        cg.add(var.set_file(template_))
+    if CONF_LOOP in config:
+        template_ = yield cg.templatable(config[CONF_LOOP], args, float)
+        cg.add(var.set_loop(template_))
+    yield var
+
+
+@automation.register_action('dfplayer.set_device', SetDeviceAction, cv.maybe_simple_value({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+    cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True),
+}, key=CONF_DEVICE))
+def dfplayer_set_device_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    template_ = yield cg.templatable(config[CONF_DEVICE], args, Device)
+    cg.add(var.set_device(template_))
+    yield var
+
+
+@automation.register_action('dfplayer.set_volume', SetVolumeAction, cv.maybe_simple_value({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+    cv.Required(CONF_VOLUME): cv.templatable(cv.int_),
+}, key=CONF_VOLUME))
+def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    template_ = yield cg.templatable(config[CONF_VOLUME], args, float)
+    cg.add(var.set_volume(template_))
+    yield var
+
+
+@automation.register_action('dfplayer.set_eq', SetEqAction, cv.maybe_simple_value({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+    cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)),
+}, key=CONF_EQ_PRESET))
+def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset)
+    cg.add(var.set_eq(template_))
+    yield var
+
+
+@automation.register_action('dfplayer.sleep', SleepAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_sleep_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.reset', ResetAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_reset_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.start', StartAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_start_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.pause', PauseAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_pause_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.stop', StopAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_stop_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_action('dfplayer.random', RandomAction, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplayer_random_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
+
+
+@automation.register_condition('dfplayer.is_playing', DFPlayerIsPlayingCondition, cv.Schema({
+    cv.GenerateID(): cv.use_id(DFPlayer),
+}))
+def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args):
+    var = cg.new_Pvariable(condition_id, template_arg)
+    yield cg.register_parented(var, config[CONF_ID])
+    yield var
diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp
new file mode 100644
index 0000000000..5ce4998796
--- /dev/null
+++ b/esphome/components/dfplayer/dfplayer.cpp
@@ -0,0 +1,124 @@
+#include "dfplayer.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace dfplayer {
+
+static const char* TAG = "dfplayer";
+
+void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
+  if (folder < 100 && file < 256) {
+    this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file);
+  } else if (folder <= 10 && file <= 1000) {
+    this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file);
+  } else {
+    ESP_LOGE(TAG, "Cannot play folder %d file %d.", folder, file);
+  }
+}
+
+void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
+  uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef};
+  uint16_t checksum = 0;
+  for (uint8_t i = 1; i < 7; i++)
+    checksum += buffer[i];
+  checksum = -checksum;
+  buffer[7] = checksum >> 8;
+  buffer[8] = (uint8_t) checksum;
+
+  this->sent_cmd_ = cmd;
+
+  ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument);
+  this->write_array(buffer, 10);
+}
+
+void DFPlayer::loop() {
+  // Read message
+  while (this->available()) {
+    uint8_t byte;
+    this->read_byte(&byte);
+
+    if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
+      this->read_pos_ = 0;
+
+    switch (this->read_pos_) {
+      case 0:  // Start mark
+        if (byte != 0x7E)
+          continue;
+        break;
+      case 1:  // Version
+        if (byte != 0xFF) {
+          ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
+          this->read_pos_ = 0;
+          continue;
+        }
+        break;
+      case 2:  // Buffer length
+        if (byte != 0x06) {
+          ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
+          this->read_pos_ = 0;
+          continue;
+        }
+        break;
+      case 9:  // End byte
+        if (byte != 0xEF) {
+          ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
+          this->read_pos_ = 0;
+          continue;
+        }
+        // Parse valid received command
+        uint8_t cmd = this->read_buffer_[3];
+        uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
+
+        ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
+
+        switch (cmd) {
+          case 0x3A:
+            if (argument == 1) {
+              ESP_LOGI(TAG, "USB loaded");
+            } else if (argument == 2)
+              ESP_LOGI(TAG, "TF Card loaded");
+            break;
+          case 0x3B:
+            if (argument == 1) {
+              ESP_LOGI(TAG, "USB unloaded");
+            } else if (argument == 2)
+              ESP_LOGI(TAG, "TF Card unloaded");
+            break;
+          case 0x3F:
+            if (argument == 1) {
+              ESP_LOGI(TAG, "USB available");
+            } else if (argument == 2) {
+              ESP_LOGI(TAG, "TF Card available");
+            } else if (argument == 3) {
+              ESP_LOGI(TAG, "USB, TF Card available");
+            }
+            break;
+          case 0x41:
+            ESP_LOGV(TAG, "Ack ok");
+            this->is_playing_ |= this->ack_set_is_playing_;
+            this->is_playing_ &= !this->ack_reset_is_playing_;
+            this->ack_set_is_playing_ = false;
+            this->ack_reset_is_playing_ = false;
+            break;
+          case 0x3D:  // Playback finished
+            this->is_playing_ = false;
+            this->on_finished_playback_callback_.call();
+            break;
+          default:
+            ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument);
+        }
+        this->sent_cmd_ = 0;
+        this->read_pos_ = 0;
+        continue;
+    }
+    this->read_buffer_[this->read_pos_] = byte;
+    this->read_pos_++;
+  }
+}
+void DFPlayer::dump_config() {
+  ESP_LOGCONFIG(TAG, "DFPlayer:");
+  this->check_uart_settings(9600);
+}
+
+}  // namespace dfplayer
+}  // namespace esphome
diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h
new file mode 100644
index 0000000000..86efd62138
--- /dev/null
+++ b/esphome/components/dfplayer/dfplayer.h
@@ -0,0 +1,166 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/uart/uart.h"
+#include "esphome/core/automation.h"
+
+const size_t DFPLAYER_READ_BUFFER_LENGTH = 25;  // two messages + some extra
+
+namespace esphome {
+namespace dfplayer {
+
+enum EqPreset {
+  NORMAL = 0,
+  POP = 1,
+  ROCK = 2,
+  JAZZ = 3,
+  CLASSIC = 4,
+  BASS = 5,
+};
+
+enum Device {
+  USB = 1,
+  TF_CARD = 2,
+};
+
+class DFPlayer : public uart::UARTDevice, public Component {
+ public:
+  void loop() override;
+
+  void next() { this->send_cmd_(0x01); }
+  void previous() { this->send_cmd_(0x02); }
+  void play_file(uint16_t file) {
+    this->ack_set_is_playing_ = true;
+    this->send_cmd_(0x03, file);
+  }
+  void play_file_loop(uint16_t file) { this->send_cmd_(0x08, file); }
+  void play_folder(uint16_t folder, uint16_t file);
+  void play_folder_loop(uint16_t folder) { this->send_cmd_(0x17, folder); }
+  void volume_up() { this->send_cmd_(0x04); }
+  void volume_down() { this->send_cmd_(0x05); }
+  void set_device(Device device) { this->send_cmd_(0x09, device); }
+  void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); }
+  void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); }
+  void sleep() { this->send_cmd_(0x0A); }
+  void reset() { this->send_cmd_(0x0C); }
+  void start() { this->send_cmd_(0x0D); }
+  void pause() {
+    this->ack_reset_is_playing_ = true;
+    this->send_cmd_(0x0E);
+  }
+  void stop() { this->send_cmd_(0x16); }
+  void random() { this->send_cmd_(0x18); }
+
+  bool is_playing() { return is_playing_; }
+  void dump_config() override;
+
+  void add_on_finished_playback_callback(std::function<void()> callback) {
+    this->on_finished_playback_callback_.add(std::move(callback));
+  }
+
+ protected:
+  void send_cmd_(uint8_t cmd, uint16_t argument = 0);
+  void send_cmd_(uint8_t cmd, uint16_t high, uint16_t low) {
+    this->send_cmd_(cmd, ((high & 0xFF) << 8) | (low & 0xFF));
+  }
+  uint8_t sent_cmd_{0};
+
+  char read_buffer_[DFPLAYER_READ_BUFFER_LENGTH];
+  size_t read_pos_{0};
+
+  bool is_playing_{false};
+  bool ack_set_is_playing_{false};
+  bool ack_reset_is_playing_{false};
+
+  CallbackManager<void()> on_finished_playback_callback_;
+};
+
+#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \
+  template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<DFPlayer> { \
+   public: \
+    void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \
+  };
+
+DFPLAYER_SIMPLE_ACTION(NextAction, next)
+DFPLAYER_SIMPLE_ACTION(PreviousAction, previous)
+
+template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Parented<DFPlayer> {
+ public:
+  TEMPLATABLE_VALUE(uint16_t, file)
+  TEMPLATABLE_VALUE(boolean, loop)
+  void play(Ts... x) override {
+    auto file = this->file_.value(x...);
+    auto loop = this->loop_.value(x...);
+    if (loop) {
+      this->parent_->play_file_loop(file);
+    } else {
+      this->parent_->play_file(file);
+    }
+  }
+};
+
+template<typename... Ts> class PlayFolderAction : public Action<Ts...>, public Parented<DFPlayer> {
+ public:
+  TEMPLATABLE_VALUE(uint16_t, folder)
+  TEMPLATABLE_VALUE(uint16_t, file)
+  TEMPLATABLE_VALUE(boolean, loop)
+  void play(Ts... x) override {
+    auto folder = this->folder_.value(x...);
+    auto file = this->file_.value(x...);
+    auto loop = this->loop_.value(x...);
+    if (loop) {
+      this->parent_->play_folder_loop(folder);
+    } else {
+      this->parent_->play_folder(folder, file);
+    }
+  }
+};
+
+template<typename... Ts> class SetDeviceAction : public Action<Ts...>, public Parented<DFPlayer> {
+ public:
+  TEMPLATABLE_VALUE(Device, device)
+  void play(Ts... x) override {
+    auto device = this->device_.value(x...);
+    this->parent_->set_device(device);
+  }
+};
+
+template<typename... Ts> class SetVolumeAction : public Action<Ts...>, public Parented<DFPlayer> {
+ public:
+  TEMPLATABLE_VALUE(uint8_t, volume)
+  void play(Ts... x) override {
+    auto volume = this->volume_.value(x...);
+    this->parent_->set_volume(volume);
+  }
+};
+
+template<typename... Ts> class SetEqAction : public Action<Ts...>, public Parented<DFPlayer> {
+ public:
+  TEMPLATABLE_VALUE(EqPreset, eq)
+  void play(Ts... x) override {
+    auto eq = this->eq_.value(x...);
+    this->parent_->set_eq(eq);
+  }
+};
+
+DFPLAYER_SIMPLE_ACTION(SleepAction, sleep)
+DFPLAYER_SIMPLE_ACTION(ResetAction, reset)
+DFPLAYER_SIMPLE_ACTION(StartAction, start)
+DFPLAYER_SIMPLE_ACTION(PauseAction, pause)
+DFPLAYER_SIMPLE_ACTION(StopAction, stop)
+DFPLAYER_SIMPLE_ACTION(RandomAction, random)
+
+template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> {
+ public:
+  bool check(Ts... x) override { return this->parent_->is_playing(); }
+};
+
+class DFPlayerFinishedPlaybackTrigger : public Trigger<> {
+ public:
+  explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) {
+    parent->add_on_finished_playback_callback([this]() { this->trigger(); });
+  }
+};
+
+}  // namespace dfplayer
+}  // namespace esphome
diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp
index 797137c08c..1e28246bee 100644
--- a/esphome/components/dht/dht.cpp
+++ b/esphome/components/dht/dht.cpp
@@ -56,6 +56,8 @@ void DHT::update() {
       str = " and consider manually specifying the DHT model using the model option";
     }
     ESP_LOGW(TAG, "Invalid readings! Please check your wiring (pull-up resistor, pin number)%s.", str);
+    this->temperature_sensor_->publish_state(NAN);
+    this->humidity_sensor_->publish_state(NAN);
     this->status_set_warning();
   }
 }
@@ -153,13 +155,15 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro
 
   if (checksum_a != data[4] && checksum_b != data[4]) {
     if (report_errors) {
-      ESP_LOGE(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]);
+      ESP_LOGW(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]);
     }
     return false;
   }
 
   if (this->model_ == DHT_MODEL_DHT11) {
     *humidity = data[0];
+    if (*humidity > 100)
+      *humidity = NAN;
     *temperature = data[2];
   } else {
     uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
@@ -170,18 +174,20 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro
 
     if (raw_temperature == 1 && raw_humidity == 10) {
       if (report_errors) {
-        ESP_LOGE(TAG, "Invalid temperature+humidity! Sensor reported 1°C and 1%% Hum");
+        ESP_LOGW(TAG, "Invalid temperature+humidity! Sensor reported 1°C and 1%% Hum");
       }
       return false;
     }
 
     *humidity = raw_humidity * 0.1f;
+    if (*humidity > 100)
+      *humidity = NAN;
     *temperature = int16_t(raw_temperature) * 0.1f;
   }
 
   if (*temperature == 0.0f && (*humidity == 1.0f || *humidity == 2.0f)) {
     if (report_errors) {
-      ESP_LOGE(TAG, "DHT reports invalid data. Is the update interval too high or the sensor damaged?");
+      ESP_LOGW(TAG, "DHT reports invalid data. Is the update interval too high or the sensor damaged?");
     }
     return false;
   }
diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py
index e1e18bb7f9..8455f74fb4 100644
--- a/esphome/components/dht/sensor.py
+++ b/esphome/components/dht/sensor.py
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import sensor
 from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, \
-    CONF_UPDATE_INTERVAL, ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT
+    ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT
 from esphome.cpp_helpers import gpio_pin_expression
 
 dht_ns = cg.esphome_ns.namespace('dht')
@@ -24,7 +24,6 @@ CONFIG_SCHEMA = cv.Schema({
     cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
     cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0),
     cv.Optional(CONF_MODEL, default='auto detect'): cv.enum(DHT_MODELS, upper=True, space='_'),
-    cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval,
 }).extend(cv.polling_component_schema('60s'))
 
 
diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py
index 5c204fc7a4..38d19d832e 100644
--- a/esphome/components/display/__init__.py
+++ b/esphome/components/display/__init__.py
@@ -3,7 +3,7 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import core, automation
 from esphome.automation import maybe_simple_id
-from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION, CONF_UPDATE_INTERVAL
+from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION
 from esphome.core import coroutine, coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
@@ -33,7 +33,6 @@ def validate_rotation(value):
 
 
 BASIC_DISPLAY_SCHEMA = cv.Schema({
-    cv.Optional(CONF_UPDATE_INTERVAL): cv.update_interval,
     cv.Optional(CONF_LAMBDA): cv.lambda_,
 })
 
@@ -48,8 +47,6 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend({
 
 @coroutine
 def setup_display_core_(var, config):
-    if CONF_UPDATE_INTERVAL in config:
-        cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
     if CONF_ROTATION in config:
         cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
     if CONF_PAGES in config:
diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h
index 57b95eee29..b12fad8c8a 100644
--- a/esphome/components/display/display_buffer.h
+++ b/esphome/components/display/display_buffer.h
@@ -84,8 +84,8 @@ using display_writer_t = std::function<void(DisplayBuffer &)>;
 #define LOG_DISPLAY(prefix, type, obj) \
   if (obj != nullptr) { \
     ESP_LOGCONFIG(TAG, prefix type); \
-    ESP_LOGCONFIG(TAG, prefix "  Rotations: %d °", obj->rotation_); \
-    ESP_LOGCONFIG(TAG, prefix "  Dimensions: %dpx x %dpx", obj->get_width(), obj->get_height()); \
+    ESP_LOGCONFIG(TAG, "%s  Rotations: %d °", prefix, obj->rotation_); \
+    ESP_LOGCONFIG(TAG, "%s  Dimensions: %dpx x %dpx", prefix, obj->get_width(), obj->get_height()); \
   }
 
 class DisplayBuffer {
diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
index 1f3bf01a86..b7810bd056 100644
--- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
+++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
@@ -31,6 +31,11 @@ static esp_ble_adv_params_t ble_adv_params = {
 static esp_ble_ibeacon_head_t ibeacon_common_head = {
     .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502};
 
+void ESP32BLEBeacon::dump_config() {
+  ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:");
+  ESP_LOGCONFIG(TAG, "  Major: %u, Minor: %u", this->major_, this->minor_);
+}
+
 void ESP32BLEBeacon::setup() {
   ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon...");
   global_esp32_ble_beacon = this;
@@ -50,7 +55,7 @@ void ESP32BLEBeacon::ble_core_task(void *params) {
   ble_setup();
 
   while (true) {
-    delay(1000);
+    delay(1000);  // NOLINT
   }
 }
 void ESP32BLEBeacon::ble_setup() {
diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h
index 1c52b41d73..aba02830b3 100644
--- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h
+++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h
@@ -34,6 +34,7 @@ class ESP32BLEBeacon : public Component {
   explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {}
 
   void setup() override;
+  void dump_config() override;
   float get_setup_priority() const override;
 
   void set_major(uint16_t major) { this->major_ = major; }
diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py
index 5a4862f733..7e998e77b1 100644
--- a/esphome/components/esp32_ble_tracker/__init__.py
+++ b/esphome/components/esp32_ble_tracker/__init__.py
@@ -1,19 +1,48 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.const import CONF_ID, CONF_SCAN_INTERVAL, ESP_PLATFORM_ESP32
+from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \
+    CONF_DURATION
 from esphome.core import coroutine
 
 ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
 AUTO_LOAD = ['xiaomi_ble']
 
 CONF_ESP32_BLE_ID = 'esp32_ble_id'
+CONF_SCAN_PARAMETERS = 'scan_parameters'
+CONF_WINDOW = 'window'
+CONF_ACTIVE = 'active'
 esp32_ble_tracker_ns = cg.esphome_ns.namespace('esp32_ble_tracker')
 ESP32BLETracker = esp32_ble_tracker_ns.class_('ESP32BLETracker', cg.Component)
 ESPBTDeviceListener = esp32_ble_tracker_ns.class_('ESPBTDeviceListener')
 
+
+def validate_scan_parameters(config):
+    duration = config[CONF_DURATION]
+    interval = config[CONF_INTERVAL]
+    window = config[CONF_WINDOW]
+
+    if window > interval:
+        raise cv.Invalid("Scan window ({}) needs to be smaller than scan interval ({})"
+                         "".format(window, interval))
+
+    if interval.total_milliseconds * 3 > duration.total_milliseconds:
+        raise cv.Invalid("Scan duration needs to be at least three times the scan interval to"
+                         "cover all BLE channels.")
+
+    return config
+
+
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.declare_id(ESP32BLETracker),
-    cv.Optional(CONF_SCAN_INTERVAL, default='300s'): cv.positive_time_period_seconds,
+    cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({
+        cv.Optional(CONF_DURATION, default='5min'): cv.positive_time_period_seconds,
+        cv.Optional(CONF_INTERVAL, default='320ms'): cv.positive_time_period_milliseconds,
+        cv.Optional(CONF_WINDOW, default='200ms'): cv.positive_time_period_milliseconds,
+        cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
+    }), validate_scan_parameters),
+
+    cv.Optional('scan_interval'): cv.invalid("This option has been removed in 1.14 (Reason: "
+                                             "it never had an effect)"),
 }).extend(cv.COMPONENT_SCHEMA)
 
 ESP_BLE_DEVICE_SCHEMA = cv.Schema({
@@ -24,7 +53,11 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema({
 def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     yield cg.register_component(var, config)
-    cg.add(var.set_scan_interval(config[CONF_SCAN_INTERVAL]))
+    params = config[CONF_SCAN_PARAMETERS]
+    cg.add(var.set_scan_duration(params[CONF_DURATION]))
+    cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625)))
+    cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625)))
+    cg.add(var.set_scan_active(params[CONF_ACTIVE]))
 
 
 @coroutine
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index 3aaf8eaa44..7a5bd733a2 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -1,6 +1,7 @@
 #include "esp32_ble_tracker.h"
 #include "esphome/core/log.h"
 #include "esphome/core/application.h"
+#include "esphome/core/helpers.h"
 
 #ifdef ARDUINO_ARCH_ESP32
 
@@ -133,14 +134,14 @@ bool ESP32BLETracker::ble_setup() {
   }
 
   // BLE takes some time to be fully set up, 200ms should be more than enough
-  delay(200);
+  delay(200);  // NOLINT
 
   return true;
 }
 
 void ESP32BLETracker::start_scan(bool first) {
   if (!xSemaphoreTake(this->scan_end_lock_, 0L)) {
-    ESP_LOGW("Cannot start scan!");
+    ESP_LOGW(TAG, "Cannot start scan!");
     return;
   }
 
@@ -150,21 +151,16 @@ void ESP32BLETracker::start_scan(bool first) {
       listener->on_scan_end();
   }
   this->already_discovered_.clear();
-  this->scan_params_.scan_type = BLE_SCAN_TYPE_ACTIVE;
+  this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
   this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
   this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
-  // Values determined empirically, higher scan intervals and lower scan windows make the ESP more stable
-  // Ideally, these values should both be quite low, especially scan window. 0x10/0x10 is the esp-idf
-  // default and works quite well. 0x100/0x50 discovers a few less BLE broadcast packets but is a lot
-  // more stable (order of several hours). The old ESPHome default (1600/1600) was terrible with
-  // crashes every few minutes
-  this->scan_params_.scan_interval = 0x200;
-  this->scan_params_.scan_window = 0x30;
+  this->scan_params_.scan_interval = this->scan_interval_;
+  this->scan_params_.scan_window = this->scan_window_;
 
   esp_ble_gap_set_scan_params(&this->scan_params_);
-  esp_ble_gap_start_scanning(this->scan_interval_);
+  esp_ble_gap_start_scanning(this->scan_duration_);
 
-  this->set_timeout("scan", this->scan_interval_ * 2000, []() {
+  this->set_timeout("scan", this->scan_duration_ * 2000, []() {
     ESP_LOGW(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack...");
     App.reboot();
   });
@@ -207,20 +203,8 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res
   }
 }
 
-std::string hexencode(const std::string &raw_data) {
-  char buf[20];
-  std::string res;
-  for (size_t i = 0; i < raw_data.size(); i++) {
-    if (i + 1 != raw_data.size()) {
-      sprintf(buf, "0x%02X.", static_cast<uint8_t>(raw_data[i]));
-    } else {
-      sprintf(buf, "0x%02X ", static_cast<uint8_t>(raw_data[i]));
-    }
-    res += buf;
-  }
-  sprintf(buf, "(%zu)", raw_data.size());
-  res += buf;
-  return res;
+std::string hexencode_string(const std::string &raw_data) {
+  return hexencode(reinterpret_cast<const uint8_t *>(raw_data.c_str()), raw_data.size());
 }
 
 ESPBTUUID::ESPBTUUID() : uuid_() {}
@@ -332,15 +316,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
   for (auto uuid : this->service_uuids_) {
     ESP_LOGVV(TAG, "  Service UUID: %s", uuid.to_string().c_str());
   }
-  ESP_LOGVV(TAG, "  Manufacturer data: %s", hexencode(this->manufacturer_data_).c_str());
-  ESP_LOGVV(TAG, "  Service data: %s", hexencode(this->service_data_).c_str());
+  ESP_LOGVV(TAG, "  Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str());
+  ESP_LOGVV(TAG, "  Service data: %s", hexencode_string(this->service_data_).c_str());
 
   if (this->service_data_uuid_.has_value()) {
     ESP_LOGVV(TAG, "  Service Data UUID: %s", this->service_data_uuid_->to_string().c_str());
   }
 
   ESP_LOGVV(TAG, "Adv data: %s",
-            hexencode(std::string(reinterpret_cast<const char *>(param.ble_adv), param.adv_data_len)).c_str());
+            hexencode_string(std::string(reinterpret_cast<const char *>(param.ble_adv), param.adv_data_len)).c_str());
 #endif
 }
 void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
@@ -454,10 +438,12 @@ const std::string &ESPBTDevice::get_manufacturer_data() const { return this->man
 const std::string &ESPBTDevice::get_service_data() const { return this->service_data_; }
 const optional<ESPBTUUID> &ESPBTDevice::get_service_data_uuid() const { return this->service_data_uuid_; }
 
-void ESP32BLETracker::set_scan_interval(uint32_t scan_interval) { this->scan_interval_ = scan_interval; }
 void ESP32BLETracker::dump_config() {
   ESP_LOGCONFIG(TAG, "BLE Tracker:");
-  ESP_LOGCONFIG(TAG, "  Scan Interval: %u s", this->scan_interval_);
+  ESP_LOGCONFIG(TAG, "  Scan Duration: %u s", this->scan_duration_);
+  ESP_LOGCONFIG(TAG, "  Scan Interval: %u ms", this->scan_interval_);
+  ESP_LOGCONFIG(TAG, "  Scan Window: %u ms", this->scan_window_);
+  ESP_LOGCONFIG(TAG, "  Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE");
 }
 void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
   const uint64_t address = device.address_uint64();
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
index f1bcada621..82e8e553fc 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
@@ -107,7 +107,10 @@ class ESPBTDeviceListener {
 
 class ESP32BLETracker : public Component {
  public:
-  void set_scan_interval(uint32_t scan_interval);
+  void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
+  void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; }
+  void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; }
+  void set_scan_active(bool scan_active) { scan_active_ = scan_active; }
 
   /// Setup the FreeRTOS task and the Bluetooth stack.
   void setup() override;
@@ -142,7 +145,10 @@ class ESP32BLETracker : public Component {
   /// A structure holding the ESP BLE scan parameters.
   esp_ble_scan_params_t scan_params_;
   /// The interval in seconds to perform scans.
-  uint32_t scan_interval_{300};
+  uint32_t scan_duration_;
+  uint32_t scan_interval_;
+  uint32_t scan_window_;
+  bool scan_active_;
   SemaphoreHandle_t scan_result_lock_;
   SemaphoreHandle_t scan_end_lock_;
   size_t scan_result_index_{0};
diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py
index c69e0a5710..81980d9d38 100644
--- a/esphome/components/esp32_camera/__init__.py
+++ b/esphome/components/esp32_camera/__init__.py
@@ -2,7 +2,7 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
 from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, \
-    ESP_PLATFORM_ESP32
+    ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS
 
 ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
 DEPENDENCIES = ['api']
@@ -35,23 +35,19 @@ FRAME_SIZES = {
     'UXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200,
 }
 
-CONF_DATA_PINS = 'data_pins'
 CONF_VSYNC_PIN = 'vsync_pin'
 CONF_HREF_PIN = 'href_pin'
 CONF_PIXEL_CLOCK_PIN = 'pixel_clock_pin'
 CONF_EXTERNAL_CLOCK = 'external_clock'
 CONF_I2C_PINS = 'i2c_pins'
-CONF_RESET_PIN = 'reset_pin'
 CONF_POWER_DOWN_PIN = 'power_down_pin'
 
 CONF_MAX_FRAMERATE = 'max_framerate'
 CONF_IDLE_FRAMERATE = 'idle_framerate'
-CONF_RESOLUTION = 'resolution'
 CONF_JPEG_QUALITY = 'jpeg_quality'
 CONF_VERTICAL_FLIP = 'vertical_flip'
 CONF_HORIZONTAL_MIRROR = 'horizontal_mirror'
 CONF_CONTRAST = 'contrast'
-CONF_BRIGHTNESS = 'brightness'
 CONF_SATURATION = 'saturation'
 CONF_TEST_PATTERN = 'test_pattern'
 
diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp
index e85d0ef5c2..ce0028159e 100644
--- a/esphome/components/esp32_touch/esp32_touch.cpp
+++ b/esphome/components/esp32_touch/esp32_touch.cpp
@@ -108,6 +108,8 @@ void ESP32TouchComponent::dump_config() {
 }
 
 void ESP32TouchComponent::loop() {
+  const uint32_t now = millis();
+  bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
   for (auto *child : this->children_) {
     uint16_t value;
     if (this->iir_filter_enabled_()) {
@@ -116,35 +118,20 @@ void ESP32TouchComponent::loop() {
       touch_pad_read(child->get_touch_pad(), &value);
     }
 
+    child->value_ = value;
     child->publish_state(value < child->get_threshold());
 
-    if (this->setup_mode_) {
+    if (should_print) {
       ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value);
     }
   }
 
-  if (this->setup_mode_) {
+  if (should_print) {
     // Avoid spamming logs
-    delay(250);
+    this->setup_mode_last_log_print_ = now;
   }
 }
-void ESP32TouchComponent::register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); }
-void ESP32TouchComponent::set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; }
-bool ESP32TouchComponent::iir_filter_enabled_() const { return this->iir_filter_ > 0; }
 
-void ESP32TouchComponent::set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; }
-float ESP32TouchComponent::get_setup_priority() const { return setup_priority::DATA; }
-void ESP32TouchComponent::set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; }
-void ESP32TouchComponent::set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; }
-void ESP32TouchComponent::set_low_voltage_reference(touch_low_volt_t low_voltage_reference) {
-  this->low_voltage_reference_ = low_voltage_reference;
-}
-void ESP32TouchComponent::set_high_voltage_reference(touch_high_volt_t high_voltage_reference) {
-  this->high_voltage_reference_ = high_voltage_reference;
-}
-void ESP32TouchComponent::set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) {
-  this->voltage_attenuation_ = voltage_attenuation;
-}
 void ESP32TouchComponent::on_shutdown() {
   if (this->iir_filter_enabled_()) {
     touch_pad_filter_stop();
@@ -155,8 +142,6 @@ void ESP32TouchComponent::on_shutdown() {
 
 ESP32TouchBinarySensor::ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold)
     : BinarySensor(name), touch_pad_(touch_pad), threshold_(threshold) {}
-touch_pad_t ESP32TouchBinarySensor::get_touch_pad() const { return this->touch_pad_; }
-uint16_t ESP32TouchBinarySensor::get_threshold() const { return this->threshold_; }
 
 }  // namespace esp32_touch
 }  // namespace esphome
diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h
index b68876c33e..45d459a2ff 100644
--- a/esphome/components/esp32_touch/esp32_touch.h
+++ b/esphome/components/esp32_touch/esp32_touch.h
@@ -12,32 +12,36 @@ class ESP32TouchBinarySensor;
 
 class ESP32TouchComponent : public Component {
  public:
-  void register_touch_pad(ESP32TouchBinarySensor *pad);
+  void register_touch_pad(ESP32TouchBinarySensor *pad) { children_.push_back(pad); }
 
-  void set_setup_mode(bool setup_mode);
+  void set_setup_mode(bool setup_mode) { setup_mode_ = setup_mode; }
 
-  void set_iir_filter(uint32_t iir_filter);
+  void set_iir_filter(uint32_t iir_filter) { iir_filter_ = iir_filter; }
 
-  void set_sleep_duration(uint16_t sleep_duration);
+  void set_sleep_duration(uint16_t sleep_duration) { sleep_cycle_ = sleep_duration; }
 
-  void set_measurement_duration(uint16_t meas_cycle);
+  void set_measurement_duration(uint16_t meas_cycle) { meas_cycle_ = meas_cycle; }
 
-  void set_low_voltage_reference(touch_low_volt_t low_voltage_reference);
+  void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) {
+    low_voltage_reference_ = low_voltage_reference;
+  }
 
-  void set_high_voltage_reference(touch_high_volt_t high_voltage_reference);
+  void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) {
+    high_voltage_reference_ = high_voltage_reference;
+  }
 
-  void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation);
+  void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { voltage_attenuation_ = voltage_attenuation; }
 
   void setup() override;
   void dump_config() override;
   void loop() override;
-  float get_setup_priority() const override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
 
   void on_shutdown() override;
 
  protected:
   /// Is the IIR filter enabled?
-  bool iir_filter_enabled_() const;
+  bool iir_filter_enabled_() const { return iir_filter_ > 0; }
 
   uint16_t sleep_cycle_{};
   uint16_t meas_cycle_{65535};
@@ -46,6 +50,7 @@ class ESP32TouchComponent : public Component {
   touch_volt_atten_t voltage_attenuation_{};
   std::vector<ESP32TouchBinarySensor *> children_;
   bool setup_mode_{false};
+  uint32_t setup_mode_last_log_print_{};
   uint32_t iir_filter_{0};
 };
 
@@ -54,14 +59,17 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
  public:
   ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold);
 
-  touch_pad_t get_touch_pad() const;
-  uint16_t get_threshold() const;
+  touch_pad_t get_touch_pad() const { return touch_pad_; }
+  uint16_t get_threshold() const { return threshold_; }
+  void set_threshold(uint16_t threshold) { threshold_ = threshold; }
+  uint16_t get_value() const { return value_; }
 
  protected:
   friend ESP32TouchComponent;
 
   touch_pad_t touch_pad_;
   uint16_t threshold_;
+  uint16_t value_;
 };
 
 }  // namespace esp32_touch
diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py
index 7354f9ae9f..b552c917c0 100644
--- a/esphome/components/fastled_base/__init__.py
+++ b/esphome/components/fastled_base/__init__.py
@@ -34,5 +34,6 @@ def new_fastled_light(config):
         cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
 
     yield light.register_light(var, config)
-    cg.add_library('FastLED', '3.2.0')
+    # https://github.com/FastLED/FastLED/blob/master/library.json
+    cg.add_library('FastLED', '3.2.9')
     yield var
diff --git a/esphome/components/fujitsu_general/__init__.py b/esphome/components/fujitsu_general/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py
new file mode 100644
index 0000000000..a6774c397a
--- /dev/null
+++ b/esphome/components/fujitsu_general/climate.py
@@ -0,0 +1,18 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import climate_ir
+from esphome.const import CONF_ID
+
+AUTO_LOAD = ['climate_ir']
+
+fujitsu_general_ns = cg.esphome_ns.namespace('fujitsu_general')
+FujitsuGeneralClimate = fujitsu_general_ns.class_('FujitsuGeneralClimate', climate_ir.ClimateIR)
+
+CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate),
+})
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp
new file mode 100644
index 0000000000..261d8be258
--- /dev/null
+++ b/esphome/components/fujitsu_general/fujitsu_general.cpp
@@ -0,0 +1,212 @@
+#include "fujitsu_general.h"
+
+namespace esphome {
+namespace fujitsu_general {
+
+static const char *TAG = "fujitsu_general.climate";
+
+// Control packet
+const uint16_t FUJITSU_GENERAL_STATE_LENGTH = 16;
+
+const uint8_t FUJITSU_GENERAL_BASE_BYTE0 = 0x14;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE1 = 0x63;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE2 = 0x00;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE3 = 0x10;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE4 = 0x10;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE5 = 0xFE;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE6 = 0x09;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE7 = 0x30;
+
+// Temperature and POWER ON
+const uint8_t FUJITSU_GENERAL_POWER_ON_MASK_BYTE8 = 0b00000001;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE8 = 0x40;
+
+// Mode
+const uint8_t FUJITSU_GENERAL_MODE_AUTO_BYTE9 = 0x00;
+const uint8_t FUJITSU_GENERAL_MODE_HEAT_BYTE9 = 0x04;
+const uint8_t FUJITSU_GENERAL_MODE_COOL_BYTE9 = 0x01;
+const uint8_t FUJITSU_GENERAL_MODE_DRY_BYTE9 = 0x02;
+const uint8_t FUJITSU_GENERAL_MODE_FAN_BYTE9 = 0x03;
+const uint8_t FUJITSU_GENERAL_MODE_10C_BYTE9 = 0x0B;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE9 = 0x01;
+
+// Fan speed and swing
+const uint8_t FUJITSU_GENERAL_FAN_AUTO_BYTE10 = 0x00;
+const uint8_t FUJITSU_GENERAL_FAN_HIGH_BYTE10 = 0x01;
+const uint8_t FUJITSU_GENERAL_FAN_MEDIUM_BYTE10 = 0x02;
+const uint8_t FUJITSU_GENERAL_FAN_LOW_BYTE10 = 0x03;
+const uint8_t FUJITSU_GENERAL_FAN_SILENT_BYTE10 = 0x04;
+const uint8_t FUJITSU_GENERAL_SWING_MASK_BYTE10 = 0b00010000;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE10 = 0x00;
+
+const uint8_t FUJITSU_GENERAL_BASE_BYTE11 = 0x00;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE12 = 0x00;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE13 = 0x00;
+
+// Outdoor Unit Low Noise
+const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0;
+const uint8_t FUJITSU_GENERAL_BASE_BYTE14 = 0x20;
+
+// CRC
+const uint8_t FUJITSU_GENERAL_BASE_BYTE15 = 0x6F;
+
+// Power off packet is specific
+const uint16_t FUJITSU_GENERAL_OFF_LENGTH = 7;
+
+const uint8_t FUJITSU_GENERAL_OFF_BYTE0 = FUJITSU_GENERAL_BASE_BYTE0;
+const uint8_t FUJITSU_GENERAL_OFF_BYTE1 = FUJITSU_GENERAL_BASE_BYTE1;
+const uint8_t FUJITSU_GENERAL_OFF_BYTE2 = FUJITSU_GENERAL_BASE_BYTE2;
+const uint8_t FUJITSU_GENERAL_OFF_BYTE3 = FUJITSU_GENERAL_BASE_BYTE3;
+const uint8_t FUJITSU_GENERAL_OFF_BYTE4 = FUJITSU_GENERAL_BASE_BYTE4;
+const uint8_t FUJITSU_GENERAL_OFF_BYTE5 = 0x02;
+const uint8_t FUJITSU_GENERAL_OFF_BYTE6 = 0xFD;
+
+const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30;  // Celsius
+const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16;  // Celsius
+
+const uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300;
+const uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600;
+const uint16_t FUJITSU_GENERAL_BIT_MARK = 420;
+const uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200;
+const uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420;
+const uint16_t FUJITSU_GENERAL_TRL_MARK = 420;
+const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000;
+
+const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000;
+
+FujitsuGeneralClimate::FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1) {}
+
+void FujitsuGeneralClimate::transmit_state() {
+  if (this->mode == climate::CLIMATE_MODE_OFF) {
+    this->transmit_off_();
+    return;
+  }
+  uint8_t remote_state[FUJITSU_GENERAL_STATE_LENGTH] = {0};
+
+  remote_state[0] = FUJITSU_GENERAL_BASE_BYTE0;
+  remote_state[1] = FUJITSU_GENERAL_BASE_BYTE1;
+  remote_state[2] = FUJITSU_GENERAL_BASE_BYTE2;
+  remote_state[3] = FUJITSU_GENERAL_BASE_BYTE3;
+  remote_state[4] = FUJITSU_GENERAL_BASE_BYTE4;
+  remote_state[5] = FUJITSU_GENERAL_BASE_BYTE5;
+  remote_state[6] = FUJITSU_GENERAL_BASE_BYTE6;
+  remote_state[7] = FUJITSU_GENERAL_BASE_BYTE7;
+  remote_state[8] = FUJITSU_GENERAL_BASE_BYTE8;
+  remote_state[9] = FUJITSU_GENERAL_BASE_BYTE9;
+  remote_state[10] = FUJITSU_GENERAL_BASE_BYTE10;
+  remote_state[11] = FUJITSU_GENERAL_BASE_BYTE11;
+  remote_state[12] = FUJITSU_GENERAL_BASE_BYTE12;
+  remote_state[13] = FUJITSU_GENERAL_BASE_BYTE13;
+  remote_state[14] = FUJITSU_GENERAL_BASE_BYTE14;
+  remote_state[15] = FUJITSU_GENERAL_BASE_BYTE15;
+
+  // Set temperature
+  uint8_t safecelsius = std::max((uint8_t) this->target_temperature, FUJITSU_GENERAL_TEMP_MIN);
+  safecelsius = std::min(safecelsius, FUJITSU_GENERAL_TEMP_MAX);
+  remote_state[8] = (byte) safecelsius - 16;
+  remote_state[8] = remote_state[8] << 4;
+
+  // If not powered - set power on flag
+  if (!this->power_) {
+    remote_state[8] = (byte) remote_state[8] | FUJITSU_GENERAL_POWER_ON_MASK_BYTE8;
+  }
+
+  // Set mode
+  switch (this->mode) {
+    case climate::CLIMATE_MODE_COOL:
+      remote_state[9] = FUJITSU_GENERAL_MODE_COOL_BYTE9;
+      break;
+    case climate::CLIMATE_MODE_HEAT:
+      remote_state[9] = FUJITSU_GENERAL_MODE_HEAT_BYTE9;
+      break;
+    case climate::CLIMATE_MODE_AUTO:
+    default:
+      remote_state[9] = FUJITSU_GENERAL_MODE_AUTO_BYTE9;
+      break;
+      // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY, CLIMATE_MODE_10C are missing in esphome
+  }
+
+  // TODO: missing support for fan speed
+  remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10;
+
+  // TODO: missing support for swing
+  // remote_state[10] = (byte) remote_state[10] | FUJITSU_GENERAL_SWING_MASK_BYTE10;
+
+  // TODO: missing support for outdoor unit low noise
+  // remote_state[14] = (byte) remote_state[14] | FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14;
+
+  // CRC
+  remote_state[15] = 0;
+  for (int i = 7; i < 15; i++) {
+    remote_state[15] += (byte) remote_state[i];  // Addiction
+  }
+  remote_state[15] = 0x100 - remote_state[15];  // mod 256
+
+  auto transmit = this->transmitter_->transmit();
+  auto data = transmit.get_data();
+
+  data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY);
+
+  // Header
+  data->mark(FUJITSU_GENERAL_HEADER_MARK);
+  data->space(FUJITSU_GENERAL_HEADER_SPACE);
+  // Data
+  for (uint8_t i : remote_state) {
+    // Send all Bits from Byte Data in Reverse Order
+    for (uint8_t mask = 00000001; mask > 0; mask <<= 1) {  // iterate through bit mask
+      data->mark(FUJITSU_GENERAL_BIT_MARK);
+      bool bit = i & mask;
+      data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE);
+      // Next bits
+    }
+  }
+  // Footer
+  data->mark(FUJITSU_GENERAL_TRL_MARK);
+  data->space(FUJITSU_GENERAL_TRL_SPACE);
+
+  transmit.perform();
+
+  this->power_ = true;
+}
+
+void FujitsuGeneralClimate::transmit_off_() {
+  uint8_t remote_state[FUJITSU_GENERAL_OFF_LENGTH] = {0};
+
+  remote_state[0] = FUJITSU_GENERAL_OFF_BYTE0;
+  remote_state[1] = FUJITSU_GENERAL_OFF_BYTE1;
+  remote_state[2] = FUJITSU_GENERAL_OFF_BYTE2;
+  remote_state[3] = FUJITSU_GENERAL_OFF_BYTE3;
+  remote_state[4] = FUJITSU_GENERAL_OFF_BYTE4;
+  remote_state[5] = FUJITSU_GENERAL_OFF_BYTE5;
+  remote_state[6] = FUJITSU_GENERAL_OFF_BYTE6;
+
+  auto transmit = this->transmitter_->transmit();
+  auto data = transmit.get_data();
+
+  data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY);
+
+  // Header
+  data->mark(FUJITSU_GENERAL_HEADER_MARK);
+  data->space(FUJITSU_GENERAL_HEADER_SPACE);
+
+  // Data
+  for (uint8_t i : remote_state) {
+    // Send all Bits from Byte Data in Reverse Order
+    for (uint8_t mask = 00000001; mask > 0; mask <<= 1) {  // iterate through bit mask
+      data->mark(FUJITSU_GENERAL_BIT_MARK);
+      bool bit = i & mask;
+      data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE);
+      // Next bits
+    }
+  }
+  // Footer
+  data->mark(FUJITSU_GENERAL_TRL_MARK);
+  data->space(FUJITSU_GENERAL_TRL_SPACE);
+
+  transmit.perform();
+
+  this->power_ = false;
+}
+
+}  // namespace fujitsu_general
+}  // namespace esphome
diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h
new file mode 100644
index 0000000000..80db81a167
--- /dev/null
+++ b/esphome/components/fujitsu_general/fujitsu_general.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/climate_ir/climate_ir.h"
+
+namespace esphome {
+namespace fujitsu_general {
+
+class FujitsuGeneralClimate : public climate_ir::ClimateIR {
+ public:
+  FujitsuGeneralClimate();
+
+ protected:
+  /// Transmit via IR the state of this climate controller.
+  void transmit_state() override;
+  /// Transmit via IR power off command.
+  void transmit_off_();
+
+  bool power_{false};
+};
+
+}  // namespace fujitsu_general
+}  // namespace esphome
diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h
index c7d2a18d84..397c55f6c4 100644
--- a/esphome/components/globals/globals_component.h
+++ b/esphome/components/globals/globals_component.h
@@ -2,6 +2,7 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/automation.h"
+#include "esphome/core/helpers.h"
 
 namespace esphome {
 namespace globals {
@@ -64,5 +65,7 @@ template<class C, typename... Ts> class GlobalVarSetAction : public Action<Ts...
   C *parent_;
 };
 
+template<typename T> T &id(GlobalsComponent<T> *value) { return value->value(); }
+
 }  // namespace globals
 }  // namespace esphome
diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py
index 7b383cb8a9..f75bc71009 100644
--- a/esphome/components/gpio/switch/__init__.py
+++ b/esphome/components/gpio/switch/__init__.py
@@ -15,12 +15,14 @@ RESTORE_MODES = {
     'ALWAYS_ON': GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON,
 }
 
+CONF_INTERLOCK_WAIT_TIME = 'interlock_wait_time'
 CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(GPIOSwitch),
     cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
     cv.Optional(CONF_RESTORE_MODE, default='RESTORE_DEFAULT_OFF'):
         cv.enum(RESTORE_MODES, upper=True, space='_'),
     cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)),
+    cv.Optional(CONF_INTERLOCK_WAIT_TIME, default='0ms'): cv.positive_time_period_milliseconds,
 }).extend(cv.COMPONENT_SCHEMA)
 
 
@@ -40,3 +42,4 @@ def to_code(config):
             lock = yield cg.get_variable(it)
             interlock.append(lock)
         cg.add(var.set_interlock(interlock))
+        cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME]))
diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp
index d22a74847e..d87e5a61e6 100644
--- a/esphome/components/gpio/switch/gpio_switch.cpp
+++ b/esphome/components/gpio/switch/gpio_switch.cpp
@@ -69,13 +69,29 @@ void GPIOSwitch::dump_config() {
 void GPIOSwitch::write_state(bool state) {
   if (state != this->inverted_) {
     // Turning ON, check interlocking
+
+    bool found = false;
     for (auto *lock : this->interlock_) {
       if (lock == this)
         continue;
 
-      if (lock->state)
+      if (lock->state) {
         lock->turn_off();
+        found = true;
+      }
     }
+    if (found && this->interlock_wait_time_ != 0) {
+      this->set_timeout("interlock", this->interlock_wait_time_, [this, state] {
+        // Don't write directly, call the function again
+        // (some other switch may have changed state while we were waiting)
+        this->write_state(state);
+      });
+      return;
+    }
+  } else if (this->interlock_wait_time_ != 0) {
+    // If we are switched off during the interlock wait time, cancel any pending
+    // re-activations
+    this->cancel_timeout("interlock");
   }
 
   this->pin_->digital_write(state);
diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h
index ceace477b2..dc0dd9bc95 100644
--- a/esphome/components/gpio/switch/gpio_switch.h
+++ b/esphome/components/gpio/switch/gpio_switch.h
@@ -26,6 +26,7 @@ class GPIOSwitch : public switch_::Switch, public Component {
   void setup() override;
   void dump_config() override;
   void set_interlock(const std::vector<Switch *> &interlock);
+  void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
 
  protected:
   void write_state(bool state) override;
@@ -33,6 +34,7 @@ class GPIOSwitch : public switch_::Switch, public Component {
   GPIOPin *pin_;
   GPIOSwitchRestoreMode restore_mode_{GPIO_SWITCH_RESTORE_DEFAULT_OFF};
   std::vector<Switch *> interlock_;
+  uint32_t interlock_wait_time_{0};
 };
 
 }  // namespace gpio
diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py
index 3ecbc89f73..ddbd29d5f8 100644
--- a/esphome/components/gps/__init__.py
+++ b/esphome/components/gps/__init__.py
@@ -20,4 +20,6 @@ def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     yield cg.register_component(var, config)
     yield uart.register_uart_device(var, config)
-    cg.add_library('TinyGPSPlus', '1.0.2')
+
+    # https://platformio.org/lib/show/1655/TinyGPSPlus
+    cg.add_library('1655', '1.0.2')  # TinyGPSPlus, has name conflict
diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp
index 0391a9a955..26371565f3 100644
--- a/esphome/components/gps/gps.cpp
+++ b/esphome/components/gps/gps.cpp
@@ -8,5 +8,41 @@ static const char *TAG = "gps";
 
 TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); }
 
+void GPS::loop() {
+  while (this->available() && !this->has_time_) {
+    if (this->tiny_gps_.encode(this->read())) {
+      if (tiny_gps_.location.isUpdated()) {
+        ESP_LOGD(TAG, "Location:");
+        ESP_LOGD(TAG, "  Lat: %f", tiny_gps_.location.lat());
+        ESP_LOGD(TAG, "  Lon: %f", tiny_gps_.location.lng());
+      }
+
+      if (tiny_gps_.speed.isUpdated()) {
+        ESP_LOGD(TAG, "Speed:");
+        ESP_LOGD(TAG, "  %f km/h", tiny_gps_.speed.kmph());
+      }
+      if (tiny_gps_.course.isUpdated()) {
+        ESP_LOGD(TAG, "Course:");
+        ESP_LOGD(TAG, "  %f °", tiny_gps_.course.deg());
+      }
+      if (tiny_gps_.altitude.isUpdated()) {
+        ESP_LOGD(TAG, "Altitude:");
+        ESP_LOGD(TAG, "  %f m", tiny_gps_.altitude.meters());
+      }
+      if (tiny_gps_.satellites.isUpdated()) {
+        ESP_LOGD(TAG, "Satellites:");
+        ESP_LOGD(TAG, "  %d", tiny_gps_.satellites.value());
+      }
+      if (tiny_gps_.satellites.isUpdated()) {
+        ESP_LOGD(TAG, "HDOP:");
+        ESP_LOGD(TAG, "  %.2f", tiny_gps_.hdop.hdop());
+      }
+
+      for (auto *listener : this->listeners_)
+        listener->on_update(this->tiny_gps_);
+    }
+  }
+}
+
 }  // namespace gps
 }  // namespace esphome
diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h
index 7d845d1bed..84a9248bc6 100644
--- a/esphome/components/gps/gps.h
+++ b/esphome/components/gps/gps.h
@@ -27,14 +27,7 @@ class GPS : public Component, public uart::UARTDevice {
     this->listeners_.push_back(listener);
   }
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
-  void loop() override {
-    while (this->available() && !this->has_time_) {
-      if (this->tiny_gps_.encode(this->read())) {
-        for (auto *listener : this->listeners_)
-          listener->on_update(this->tiny_gps_);
-      }
-    }
-  }
+  void loop() override;
   TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; }
 
  protected:
diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp
index c6aa8adc67..468ad09bac 100644
--- a/esphome/components/gps/time/gps_time.cpp
+++ b/esphome/components/gps/time/gps_time.cpp
@@ -6,5 +6,29 @@ namespace gps {
 
 static const char *TAG = "gps.time";
 
+void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
+  if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid())
+    return;
+  if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated())
+    return;
+  if (tiny_gps.date.year() < 2019)
+    return;
+
+  time::ESPTime val{};
+  val.year = tiny_gps.date.year();
+  val.month = tiny_gps.date.month();
+  val.day_of_month = tiny_gps.date.day();
+  // Set these to valid value for  recalc_timestamp_utc - it's not used for calculation
+  val.day_of_week = 1;
+  val.day_of_year = 1;
+
+  val.hour = tiny_gps.time.hour();
+  val.minute = tiny_gps.time.minute();
+  val.second = tiny_gps.time.second();
+  val.recalc_timestamp_utc(false);
+  this->synchronize_epoch_(val.timestamp);
+  this->has_time_ = true;
+}
+
 }  // namespace gps
 }  // namespace esphome
diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h
index b09aee364f..f6462be3e0 100644
--- a/esphome/components/gps/time/gps_time.h
+++ b/esphome/components/gps/time/gps_time.h
@@ -18,20 +18,7 @@ class GPSTime : public time::RealTimeClock, public GPSListener {
   }
 
  protected:
-  void from_tiny_gps_(TinyGPSPlus &tiny_gps) {
-    if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid())
-      return;
-    time::ESPTime val{};
-    val.year = tiny_gps.date.year();
-    val.month = tiny_gps.date.month();
-    val.day_of_month = tiny_gps.date.day();
-    val.hour = tiny_gps.time.hour();
-    val.minute = tiny_gps.time.minute();
-    val.second = tiny_gps.time.second();
-    val.recalc_timestamp_utc(false);
-    this->synchronize_epoch_(val.timestamp);
-    this->has_time_ = true;
-  }
+  void from_tiny_gps_(TinyGPSPlus &tiny_gps);
   bool has_time_{false};
 };
 
diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp
index 81637039ca..4041c0c464 100644
--- a/esphome/components/hdc1080/hdc1080.cpp
+++ b/esphome/components/hdc1080/hdc1080.cpp
@@ -19,7 +19,7 @@ void HDC1080Component::setup() {
       0b00000000   // reserved
   };
 
-  if (this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) {
+  if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) {
     this->mark_failed();
     return;
   }
diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h
index b9321b51c6..4e5dc0f67f 100644
--- a/esphome/components/hlw8012/hlw8012.h
+++ b/esphome/components/hlw8012/hlw8012.h
@@ -8,6 +8,8 @@
 namespace esphome {
 namespace hlw8012 {
 
+enum HLW8012InitialMode { HLW8012_INITIAL_MODE_CURRENT = 0, HLW8012_INITIAL_MODE_VOLTAGE };
+
 class HLW8012Component : public PollingComponent {
  public:
   void setup() override;
@@ -15,6 +17,9 @@ class HLW8012Component : public PollingComponent {
   float get_setup_priority() const override;
   void update() override;
 
+  void set_initial_mode(HLW8012InitialMode initial_mode) {
+    current_mode_ = initial_mode == HLW8012_INITIAL_MODE_CURRENT;
+  }
   void set_change_mode_every(uint32_t change_mode_every) { change_mode_every_ = change_mode_every; }
   void set_current_resistor(float current_resistor) { current_resistor_ = current_resistor; }
   void set_voltage_divider(float voltage_divider) { voltage_divider_ = voltage_divider; }
diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py
index 697c34f9d2..e1f02b8fd2 100644
--- a/esphome/components/hlw8012/sensor.py
+++ b/esphome/components/hlw8012/sensor.py
@@ -2,7 +2,7 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import sensor
-from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_CURRENT, \
+from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_INITIAL_MODE, CONF_CURRENT, \
     CONF_CURRENT_RESISTOR, CONF_ID, CONF_POWER, CONF_SEL_PIN, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, \
     ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT
 
@@ -10,6 +10,11 @@ AUTO_LOAD = ['pulse_counter']
 
 hlw8012_ns = cg.esphome_ns.namespace('hlw8012')
 HLW8012Component = hlw8012_ns.class_('HLW8012Component', cg.PollingComponent)
+HLW8012InitialMode = hlw8012_ns.enum('HLW8012InitialMode')
+INITIAL_MODES = {
+    CONF_CURRENT: HLW8012InitialMode.HLW8012_INITIAL_MODE_CURRENT,
+    CONF_VOLTAGE: HLW8012InitialMode.HLW8012_INITIAL_MODE_VOLTAGE,
+}
 
 CONF_CF1_PIN = 'cf1_pin'
 CONF_CF_PIN = 'cf_pin'
@@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.Schema({
     cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
     cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
     cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(cv.uint32_t, cv.Range(min=1)),
+    cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(*INITIAL_MODES, lower=True),
 }).extend(cv.polling_component_schema('60s'))
 
 
@@ -54,3 +60,4 @@ def to_code(config):
     cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
     cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
     cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY]))
+    cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]]))
diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp
index 61c73d272b..203f6d8a24 100644
--- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp
+++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp
@@ -16,14 +16,16 @@ void HomeassistantBinarySensor::setup() {
         ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
         break;
       case PARSE_ON:
-        ESP_LOGD(TAG, "'%s': Got state ON", this->entity_id_.c_str());
-        this->publish_state(true);
-        break;
       case PARSE_OFF:
-        ESP_LOGD(TAG, "'%s': Got state OFF", this->entity_id_.c_str());
-        this->publish_state(false);
+        bool new_state = val == PARSE_ON;
+        ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
+        if (this->initial_)
+          this->publish_initial_state(new_state);
+        else
+          this->publish_state(new_state);
         break;
     }
+    this->initial_ = false;
   });
 }
 void HomeassistantBinarySensor::dump_config() {
diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h
index c2c7ec4480..e468fd00eb 100644
--- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h
+++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h
@@ -15,6 +15,7 @@ class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Com
 
  protected:
   std::string entity_id_;
+  bool initial_{true};
 };
 
 }  // namespace homeassistant
diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp
index b21fd4c0ce..e9d97690fb 100644
--- a/esphome/components/homeassistant/time/homeassistant_time.cpp
+++ b/esphome/components/homeassistant/time/homeassistant_time.cpp
@@ -22,19 +22,5 @@ void HomeassistantTime::setup() {
 
 HomeassistantTime *global_homeassistant_time = nullptr;
 
-bool GetTimeResponse::decode_32bit(uint32_t field_id, uint32_t value) {
-  switch (field_id) {
-    case 1:
-      // fixed32 epoch_seconds = 1;
-      if (global_homeassistant_time != nullptr) {
-        global_homeassistant_time->set_epoch_time(value);
-      }
-      return true;
-    default:
-      return false;
-  }
-}
-api::APIMessageType GetTimeResponse::message_type() const { return api::APIMessageType::GET_TIME_RESPONSE; }
-
 }  // namespace homeassistant
 }  // namespace esphome
diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h
index 43937c6f13..8ab09d1185 100644
--- a/esphome/components/homeassistant/time/homeassistant_time.h
+++ b/esphome/components/homeassistant/time/homeassistant_time.h
@@ -17,11 +17,5 @@ class HomeassistantTime : public time::RealTimeClock {
 
 extern HomeassistantTime *global_homeassistant_time;
 
-class GetTimeResponse : public api::APIMessage {
- public:
-  bool decode_32bit(uint32_t field_id, uint32_t value) override;
-  api::APIMessageType message_type() const override;
-};
-
 }  // namespace homeassistant
 }  // namespace esphome
diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp
index 3fa9d2c37a..562bd26771 100644
--- a/esphome/components/i2c/i2c.cpp
+++ b/esphome/components/i2c/i2c.cpp
@@ -32,7 +32,7 @@ void I2CComponent::dump_config() {
   if (this->scan_) {
     ESP_LOGI(TAG, "Scanning i2c bus for active devices...");
     uint8_t found = 0;
-    for (uint8_t address = 8; address < 120; address++) {
+    for (uint8_t address = 1; address < 120; address++) {
       this->wire_->beginTransmission(address);
       uint8_t error = this->wire_->endTransmission();
 
@@ -135,6 +135,9 @@ bool I2CComponent::read_bytes(uint8_t address, uint8_t a_register, uint8_t *data
     delay(conversion);
   return this->raw_receive(address, data, len);
 }
+bool I2CComponent::read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len) {
+  return this->raw_receive(address, data, len);
+}
 bool I2CComponent::read_bytes_16(uint8_t address, uint8_t a_register, uint16_t *data, uint8_t len,
                                  uint32_t conversion) {
   if (!this->write_bytes(address, a_register, nullptr, 0))
@@ -156,6 +159,11 @@ bool I2CComponent::write_bytes(uint8_t address, uint8_t a_register, const uint8_
   this->raw_write(address, data, len);
   return this->raw_end_transmission(address);
 }
+bool I2CComponent::write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len) {
+  this->raw_begin_transmission(address);
+  this->raw_write(address, data, len);
+  return this->raw_end_transmission(address);
+}
 bool I2CComponent::write_bytes_16(uint8_t address, uint8_t a_register, const uint16_t *data, uint8_t len) {
   this->raw_begin_transmission(address);
   this->raw_write(address, &a_register, 1);
@@ -200,5 +208,30 @@ void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; }
 uint8_t next_i2c_bus_num_ = 0;
 #endif
 
+I2CRegister &I2CRegister::operator=(uint8_t value) {
+  this->parent_->write_byte(this->register_, value);
+  return *this;
+}
+
+I2CRegister &I2CRegister::operator&=(uint8_t value) {
+  this->parent_->write_byte(this->register_, this->get() & value);
+  return *this;
+}
+
+I2CRegister &I2CRegister::operator|=(uint8_t value) {
+  this->parent_->write_byte(this->register_, this->get() | value);
+  return *this;
+}
+
+uint8_t I2CRegister::get() {
+  uint8_t value = 0x00;
+  this->parent_->read_byte(this->register_, &value);
+  return value;
+}
+I2CRegister &I2CRegister::operator=(const std::vector<uint8_t> &value) {
+  this->parent_->write_bytes(this->register_, value);
+  return *this;
+}
+
 }  // namespace i2c
 }  // namespace esphome
diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h
index e41bd6c5e8..c4ed40e268 100644
--- a/esphome/components/i2c/i2c.h
+++ b/esphome/components/i2c/i2c.h
@@ -42,6 +42,7 @@ class I2CComponent : public Component {
    * @return If the operation was successful.
    */
   bool read_bytes(uint8_t address, uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0);
+  bool read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len);
 
   /** Read len amount of 16-bit words (MSB first) from a register into data.
    *
@@ -69,6 +70,7 @@ class I2CComponent : public Component {
    * @return If the operation was successful.
    */
   bool write_bytes(uint8_t address, uint8_t a_register, const uint8_t *data, uint8_t len);
+  bool write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len);
 
   /** Write len amount of 16-bit words (MSB first) to the specified register for address.
    *
@@ -132,6 +134,24 @@ class I2CComponent : public Component {
 extern uint8_t next_i2c_bus_num_;
 #endif
 
+class I2CDevice;
+
+class I2CRegister {
+ public:
+  I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {}
+
+  I2CRegister &operator=(uint8_t value);
+  I2CRegister &operator=(const std::vector<uint8_t> &value);
+  I2CRegister &operator&=(uint8_t value);
+  I2CRegister &operator|=(uint8_t value);
+
+  uint8_t get();
+
+ protected:
+  I2CDevice *parent_;
+  uint8_t register_;
+};
+
 /** All components doing communication on the I2C bus should subclass I2CDevice.
  *
  * This class stores 1. the address of the i2c device and has a helper function to allow
@@ -151,7 +171,8 @@ class I2CDevice {
   /// Manually set the parent i2c bus for this device.
   void set_i2c_parent(I2CComponent *parent);
 
- protected:
+  I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
+
   /** Read len amount of bytes from a register into data. Optionally with a conversion time after
    * writing the register value to the bus.
    *
@@ -161,15 +182,23 @@ class I2CDevice {
    * @param conversion The time in ms between writing the register value and reading out the value.
    * @return If the operation was successful.
    */
-  bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0);  // NOLINT
+  bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0);
+  bool read_bytes_raw(uint8_t *data, uint8_t len) { return this->parent_->read_bytes_raw(this->address_, data, len); }
 
-  template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) {  // NOLINT
+  template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) {
     std::array<uint8_t, N> res;
     if (!this->read_bytes(a_register, res.data(), N)) {
       return {};
     }
     return res;
   }
+  template<size_t N> optional<std::array<uint8_t, N>> read_bytes_raw() {
+    std::array<uint8_t, N> res;
+    if (!this->read_bytes_raw(res.data(), N)) {
+      return {};
+    }
+    return res;
+  }
 
   /** Read len amount of 16-bit words (MSB first) from a register into data.
    *
@@ -179,12 +208,12 @@ class I2CDevice {
    * @param conversion The time in ms between writing the register value and reading out the value.
    * @return If the operation was successful.
    */
-  bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0);  // NOLINT
+  bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0);
 
   /// Read a single byte from a register into the data variable. Return true if successful.
-  bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0);  // NOLINT
+  bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0);
 
-  optional<uint8_t> read_byte(uint8_t a_register) {  // NOLINT
+  optional<uint8_t> read_byte(uint8_t a_register) {
     uint8_t data;
     if (!this->read_byte(a_register, &data))
       return {};
@@ -192,7 +221,7 @@ class I2CDevice {
   }
 
   /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful.
-  bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0);  // NOLINT
+  bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0);
 
   /** Write len amount of 8-bit bytes to the specified register.
    *
@@ -201,7 +230,10 @@ class I2CDevice {
    * @param len The amount of bytes to write to the bus.
    * @return If the operation was successful.
    */
-  bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len);  // NOLINT
+  bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len);
+  bool write_bytes_raw(const uint8_t *data, uint8_t len) {
+    return this->parent_->write_bytes_raw(this->address_, data, len);
+  }
 
   /** Write a vector of data to a register.
    *
@@ -209,13 +241,17 @@ class I2CDevice {
    * @param data The data to write.
    * @return If the operation was successful.
    */
-  bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) {  // NOLINT
+  bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) {
     return this->write_bytes(a_register, data.data(), data.size());
   }
+  bool write_bytes_raw(const std::vector<uint8_t> &data) { return this->write_bytes_raw(data.data(), data.size()); }
 
-  template<size_t N> bool write_bytes(uint8_t a_register, const std::array<uint8_t, N> &data) {  // NOLINT
+  template<size_t N> bool write_bytes(uint8_t a_register, const std::array<uint8_t, N> &data) {
     return this->write_bytes(a_register, data.data(), data.size());
   }
+  template<size_t N> bool write_bytes_raw(const std::array<uint8_t, N> &data) {
+    return this->write_bytes_raw(data.data(), data.size());
+  }
 
   /** Write len amount of 16-bit words (MSB first) to the specified register.
    *
@@ -224,14 +260,15 @@ class I2CDevice {
    * @param len The amount of bytes to write to the bus.
    * @return If the operation was successful.
    */
-  bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len);  // NOLINT
+  bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len);
 
   /// Write a single byte of data into the specified register. Return true if successful.
-  bool write_byte(uint8_t a_register, uint8_t data);  // NOLINT
+  bool write_byte(uint8_t a_register, uint8_t data);
 
   /// Write a single 16-bit word of data into the specified register. Return true if successful.
-  bool write_byte_16(uint8_t a_register, uint16_t data);  // NOLINT
+  bool write_byte_16(uint8_t a_register, uint16_t data);
 
+ protected:
   uint8_t address_{0x00};
   I2CComponent *parent_{nullptr};
 };
diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp
index 9ddfd2ad0b..f9b5a43870 100644
--- a/esphome/components/integration/integration_sensor.cpp
+++ b/esphome/components/integration/integration_sensor.cpp
@@ -45,14 +45,14 @@ std::string IntegrationSensor::unit_of_measurement() {
 }
 void IntegrationSensor::process_sensor_value_(float value) {
   const uint32_t now = millis();
-  const float old_value = this->last_value_;
-  const float new_value = value;
+  const double old_value = this->last_value_;
+  const double new_value = value;
   const uint32_t dt_ms = now - this->last_update_;
-  const float dt = dt_ms * this->get_time_factor_();
-  float area = 0.0f;
+  const double dt = dt_ms * this->get_time_factor_();
+  double area = 0.0f;
   switch (this->method_) {
     case INTEGRATION_METHOD_TRAPEZOID:
-      area = dt * (old_value + new_value) / 2.0f;
+      area = dt * (old_value + new_value) / 2.0;
       break;
     case INTEGRATION_METHOD_LEFT:
       area = dt * old_value;
@@ -61,7 +61,9 @@ void IntegrationSensor::process_sensor_value_(float value) {
       area = dt * new_value;
       break;
   }
-  this->publish_and_save_(this->last_value_ + area);
+  this->last_value_ = new_value;
+  this->last_update_ = now;
+  this->publish_and_save_(this->result_ + area);
 }
 
 }  // namespace integration
diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h
index 6b1f4ccf1b..2fcec069b2 100644
--- a/esphome/components/integration/integration_sensor.h
+++ b/esphome/components/integration/integration_sensor.h
@@ -51,10 +51,11 @@ class IntegrationSensor : public sensor::Sensor, public Component {
         return 0.0f;
     }
   }
-  void publish_and_save_(float result) {
+  void publish_and_save_(double result) {
     this->result_ = result;
     this->publish_state(result);
-    this->rtc_.save(&result);
+    float result_f = result;
+    this->rtc_.save(&result_f);
   }
   std::string unit_of_measurement() override;
   std::string icon() override { return this->sensor_->get_icon(); }
@@ -67,7 +68,7 @@ class IntegrationSensor : public sensor::Sensor, public Component {
   ESPPreferenceObject rtc_;
 
   uint32_t last_update_;
-  float result_{0.0f};
+  double result_{0.0f};
   float last_value_{0.0f};
 };
 
diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py
index 27f65f9336..bff194578c 100644
--- a/esphome/components/lcd_base/__init__.py
+++ b/esphome/components/lcd_base/__init__.py
@@ -1,12 +1,11 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import display
-from esphome.const import CONF_DIMENSIONS, CONF_LAMBDA
+from esphome.const import CONF_DIMENSIONS
 from esphome.core import coroutine
 
 lcd_base_ns = cg.esphome_ns.namespace('lcd_base')
 LCDDisplay = lcd_base_ns.class_('LCDDisplay', cg.PollingComponent)
-LCDDisplayRef = LCDDisplay.operator('ref')
 
 
 def validate_lcd_dimensions(value):
@@ -28,8 +27,3 @@ def setup_lcd_display(var, config):
     yield cg.register_component(var, config)
     yield display.register_display(var, config)
     cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
-
-    if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(LCDDisplayRef, 'it')],
-                                          return_type=cg.void)
-        cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp
index af6b8304eb..25ac143817 100644
--- a/esphome/components/lcd_base/lcd_display.cpp
+++ b/esphome/components/lcd_base/lcd_display.cpp
@@ -107,7 +107,7 @@ void LCDDisplay::update() {
   for (uint8_t i = 0; i < this->rows_ * this->columns_; i++)
     this->buffer_[i] = ' ';
 
-  this->writer_(*this);
+  this->call_writer();
   this->display();
 }
 void LCDDisplay::command_(uint8_t value) { this->send(value, false); }
@@ -148,6 +148,11 @@ void LCDDisplay::printf(const char *format, ...) {
   if (ret > 0)
     this->print(0, 0, buffer);
 }
+void LCDDisplay::clear() {
+  // clear display, also sets DDRAM address to 0 (home)
+  this->command_(LCD_DISPLAY_COMMAND_CLEAR_DISPLAY);
+  delay(2);
+}
 #ifdef USE_TIME
 void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) {
   char buffer[64];
diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h
index 200600eb9c..ee150059c6 100644
--- a/esphome/components/lcd_base/lcd_display.h
+++ b/esphome/components/lcd_base/lcd_display.h
@@ -12,11 +12,8 @@ namespace lcd_base {
 
 class LCDDisplay;
 
-using lcd_writer_t = std::function<void(LCDDisplay &)>;
-
 class LCDDisplay : public PollingComponent {
  public:
-  void set_writer(lcd_writer_t &&writer) { this->writer_ = std::move(writer); }
   void set_dimensions(uint8_t columns, uint8_t rows) {
     this->columns_ = columns;
     this->rows_ = rows;
@@ -26,6 +23,8 @@ class LCDDisplay : public PollingComponent {
   float get_setup_priority() const override;
   void update() override;
   void display();
+  //// Clear LCD display
+  void clear();
 
   /// Print the given text at the specified column and row.
   void print(uint8_t column, uint8_t row, const char *str);
@@ -54,11 +53,11 @@ class LCDDisplay : public PollingComponent {
   virtual void send(uint8_t value, bool rs) = 0;
 
   void command_(uint8_t value);
+  virtual void call_writer() = 0;
 
   uint8_t columns_;
   uint8_t rows_;
   uint8_t *buffer_{nullptr};
-  lcd_writer_t writer_;
 };
 
 }  // namespace lcd_base
diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py
index 1f98955ece..91498d59c9 100644
--- a/esphome/components/lcd_gpio/display.py
+++ b/esphome/components/lcd_gpio/display.py
@@ -2,7 +2,8 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import lcd_base
-from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID
+from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID, \
+    CONF_LAMBDA
 
 AUTO_LOAD = ['lcd_base']
 
@@ -42,3 +43,9 @@ def to_code(config):
     if CONF_RW_PIN in config:
         rw = yield cg.gpio_pin_expression(config[CONF_RW_PIN])
         cg.add(var.set_rw_pin(rw))
+
+    if CONF_LAMBDA in config:
+        lambda_ = yield cg.process_lambda(config[CONF_LAMBDA],
+                                          [(GPIOLCDDisplay.operator('ref'), 'it')],
+                                          return_type=cg.void)
+        cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h
index ed3b0c1137..01f6f95d9a 100644
--- a/esphome/components/lcd_gpio/gpio_lcd_display.h
+++ b/esphome/components/lcd_gpio/gpio_lcd_display.h
@@ -8,6 +8,7 @@ namespace lcd_gpio {
 
 class GPIOLCDDisplay : public lcd_base::LCDDisplay {
  public:
+  void set_writer(std::function<void(GPIOLCDDisplay &)> &&writer) { this->writer_ = std::move(writer); }
   void setup() override;
   void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) {
     this->data_pins_[0] = d0;
@@ -36,10 +37,13 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay {
   void write_n_bits(uint8_t value, uint8_t n) override;
   void send(uint8_t value, bool rs) override;
 
+  void call_writer() override { this->writer_(*this); }
+
   GPIOPin *rs_pin_{nullptr};
   GPIOPin *rw_pin_{nullptr};
   GPIOPin *enable_pin_{nullptr};
   GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
+  std::function<void(GPIOLCDDisplay &)> writer_;
 };
 
 }  // namespace lcd_gpio
diff --git a/esphome/components/lcd_pcf8574/display.py b/esphome/components/lcd_pcf8574/display.py
index 2bc04a283f..2bbb3a2f7b 100644
--- a/esphome/components/lcd_pcf8574/display.py
+++ b/esphome/components/lcd_pcf8574/display.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import lcd_base, i2c
-from esphome.const import CONF_ID
+from esphome.const import CONF_ID, CONF_LAMBDA
 
 DEPENDENCIES = ['i2c']
 AUTO_LOAD = ['lcd_base']
@@ -18,3 +18,9 @@ def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     yield lcd_base.setup_lcd_display(var, config)
     yield i2c.register_i2c_device(var, config)
+
+    if CONF_LAMBDA in config:
+        lambda_ = yield cg.process_lambda(config[CONF_LAMBDA],
+                                          [(PCF8574LCDDisplay.operator('ref'), 'it')],
+                                          return_type=cg.void)
+        cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp
index 59491c7d5a..e3002da25d 100644
--- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp
+++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp
@@ -6,9 +6,13 @@ namespace lcd_pcf8574 {
 
 static const char *TAG = "lcd_pcf8574";
 
+static const uint8_t LCD_DISPLAY_BACKLIGHT_ON = 0x08;
+static const uint8_t LCD_DISPLAY_BACKLIGHT_OFF = 0x00;
+
 void PCF8574LCDDisplay::setup() {
   ESP_LOGCONFIG(TAG, "Setting up PCF8574 LCD Display...");
-  if (!this->write_bytes(0x08, nullptr, 0)) {
+  this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON;
+  if (!this->write_bytes(this->backlight_value_, nullptr, 0)) {
     this->mark_failed();
     return;
   }
@@ -29,7 +33,7 @@ void PCF8574LCDDisplay::write_n_bits(uint8_t value, uint8_t n) {
     // Ugly fix: in the super setup() with n == 4 value needs to be shifted left
     value <<= 4;
   }
-  uint8_t data = value | 0x08;  // Enable backlight
+  uint8_t data = value | this->backlight_value_;  // Set backlight state
   this->write_bytes(data, nullptr, 0);
   // Pulse ENABLE
   this->write_bytes(data | 0x04, nullptr, 0);
@@ -41,6 +45,14 @@ void PCF8574LCDDisplay::send(uint8_t value, bool rs) {
   this->write_n_bits((value & 0xF0) | rs, 0);
   this->write_n_bits(((value << 4) & 0xF0) | rs, 0);
 }
+void PCF8574LCDDisplay::backlight() {
+  this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON;
+  this->write_bytes(this->backlight_value_, nullptr, 0);
+}
+void PCF8574LCDDisplay::no_backlight() {
+  this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_OFF;
+  this->write_bytes(this->backlight_value_, nullptr, 0);
+}
 
 }  // namespace lcd_pcf8574
 }  // namespace esphome
diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h
index 133679c501..4db3afb9b0 100644
--- a/esphome/components/lcd_pcf8574/pcf8574_display.h
+++ b/esphome/components/lcd_pcf8574/pcf8574_display.h
@@ -9,13 +9,22 @@ namespace lcd_pcf8574 {
 
 class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice {
  public:
+  void set_writer(std::function<void(PCF8574LCDDisplay &)> &&writer) { this->writer_ = std::move(writer); }
   void setup() override;
   void dump_config() override;
+  void backlight();
+  void no_backlight();
 
  protected:
   bool is_four_bit_mode() override { return true; }
   void write_n_bits(uint8_t value, uint8_t n) override;
   void send(uint8_t value, bool rs) override;
+
+  void call_writer() override { this->writer_(*this); }
+
+  // Stores the current state of the backlight.
+  uint8_t backlight_value_;
+  std::function<void(PCF8574LCDDisplay &)> writer_;
 };
 
 }  // namespace lcd_pcf8574
diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp
index 64094478c0..2b1c181a62 100644
--- a/esphome/components/ledc/ledc_output.cpp
+++ b/esphome/components/ledc/ledc_output.cpp
@@ -11,10 +11,10 @@ namespace ledc {
 static const char *TAG = "ledc.output";
 
 void LEDCOutput::write_state(float state) {
-  if (this->pin_->is_inverted()) {
+  if (this->pin_->is_inverted())
     state = 1.0f - state;
-  }
 
+  this->duty_ = state;
   const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1;
   const float duty_rounded = roundf(state * max_duty);
   auto duty = static_cast<uint32_t>(duty_rounded);
@@ -22,18 +22,45 @@ void LEDCOutput::write_state(float state) {
 }
 
 void LEDCOutput::setup() {
-  ledcSetup(this->channel_, this->frequency_, this->bit_depth_);
-  ledcAttachPin(this->pin_->get_pin(), this->channel_);
-
+  this->apply_frequency(this->frequency_);
   this->turn_off();
+  // Attach pin after setting default value
+  ledcAttachPin(this->pin_->get_pin(), this->channel_);
 }
 
 void LEDCOutput::dump_config() {
   ESP_LOGCONFIG(TAG, "LEDC Output:");
-  LOG_PIN("  Pin", this->pin_);
+  LOG_PIN("  Pin ", this->pin_);
   ESP_LOGCONFIG(TAG, "  LEDC Channel: %u", this->channel_);
   ESP_LOGCONFIG(TAG, "  Frequency: %.1f Hz", this->frequency_);
-  ESP_LOGCONFIG(TAG, "  Bit Depth: %u", this->bit_depth_);
+}
+
+float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); }
+float ledc_min_frequency_for_bit_depth(uint8_t bit_depth) {
+  const float max_div_num = ((1 << 20) - 1) / 256.0f;
+  return 80e6f / (max_div_num * float(1 << bit_depth));
+}
+optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) {
+  for (int i = 20; i >= 1; i--) {
+    const float min_frequency = ledc_min_frequency_for_bit_depth(i);
+    const float max_frequency = ledc_max_frequency_for_bit_depth(i);
+    if (min_frequency <= frequency && frequency <= max_frequency)
+      return i;
+  }
+  return {};
+}
+
+void LEDCOutput::apply_frequency(float frequency) {
+  auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency);
+  if (!bit_depth_opt.has_value()) {
+    ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency);
+    this->status_set_warning();
+  }
+  this->bit_depth_ = bit_depth_opt.value_or(8);
+  this->frequency_ = frequency;
+  ledcSetup(this->channel_, frequency, this->bit_depth_);
+  // re-apply duty
+  this->write_state(this->duty_);
 }
 
 uint8_t next_ledc_channel = 0;
diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h
index d1b9b099ee..3f56f502b0 100644
--- a/esphome/components/ledc/ledc_output.h
+++ b/esphome/components/ledc/ledc_output.h
@@ -2,6 +2,7 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/esphal.h"
+#include "esphome/core/automation.h"
 #include "esphome/components/output/float_output.h"
 
 #ifdef ARDUINO_ARCH_ESP32
@@ -16,11 +17,10 @@ class LEDCOutput : public output::FloatOutput, public Component {
   explicit LEDCOutput(GPIOPin *pin) : pin_(pin) { this->channel_ = next_ledc_channel++; }
 
   void set_channel(uint8_t channel) { this->channel_ = channel; }
-  void set_bit_depth(uint8_t bit_depth) { this->bit_depth_ = bit_depth; }
   void set_frequency(float frequency) { this->frequency_ = frequency; }
+  /// Dynamically change frequency at runtime
+  void apply_frequency(float frequency);
 
-  // ========== INTERNAL METHODS ==========
-  // (In most use cases you won't need these)
   /// Setup LEDC.
   void setup() override;
   void dump_config() override;
@@ -28,13 +28,28 @@ class LEDCOutput : public output::FloatOutput, public Component {
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
 
   /// Override FloatOutput's write_state.
-  void write_state(float adjusted_value) override;
+  void write_state(float state) override;
 
  protected:
   GPIOPin *pin_;
   uint8_t channel_{};
   uint8_t bit_depth_{};
   float frequency_{};
+  float duty_{0.0f};
+};
+
+template<typename... Ts> class SetFrequencyAction : public Action<Ts...> {
+ public:
+  SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {}
+  TEMPLATABLE_VALUE(float, frequency);
+
+  void play(Ts... x) {
+    float freq = this->frequency_.value(x...);
+    this->parent_->apply_frequency(freq);
+  }
+
+ protected:
+  LEDCOutput *parent_;
 };
 
 }  // namespace ledc
diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py
index c507465ff9..b608e9bbf7 100644
--- a/esphome/components/ledc/output.py
+++ b/esphome/components/ledc/output.py
@@ -1,6 +1,4 @@
-import math
-
-from esphome import pins
+from esphome import pins, automation
 from esphome.components import output
 import esphome.config_validation as cv
 import esphome.codegen as cg
@@ -15,53 +13,36 @@ def calc_max_frequency(bit_depth):
 
 
 def calc_min_frequency(bit_depth):
-    # LEDC_DIV_NUM_HSTIMER is 15-bit unsigned integer
-    # lower 8 bits represent fractional part
-    max_div_num = ((1 << 16) - 1) / 256.0
+    max_div_num = ((2**20) - 1) / 256.0
     return 80e6 / (max_div_num * (2**bit_depth))
 
 
-def validate_frequency_bit_depth(obj):
-    frequency = obj[CONF_FREQUENCY]
-    if CONF_BIT_DEPTH not in obj:
-        obj = obj.copy()
-        for bit_depth in range(15, 0, -1):
-            if calc_min_frequency(bit_depth) <= frequency <= calc_max_frequency(bit_depth):
-                obj[CONF_BIT_DEPTH] = bit_depth
-                break
-        else:
-            min_freq = min(calc_min_frequency(x) for x in range(1, 16))
-            max_freq = max(calc_max_frequency(x) for x in range(1, 16))
-            if frequency < min_freq:
-                raise cv.Invalid("This frequency setting is not possible, please choose a higher "
-                                 "frequency (at least {}Hz)".format(int(min_freq)))
-            if frequency > max_freq:
-                raise cv.Invalid("This frequency setting is not possible, please choose a lower "
-                                 "frequency (at most {}Hz)".format(int(max_freq)))
-            raise cv.Invalid("Invalid frequency!")
-
-    bit_depth = obj[CONF_BIT_DEPTH]
-    min_freq = calc_min_frequency(bit_depth)
-    max_freq = calc_max_frequency(bit_depth)
-    if frequency > max_freq:
-        raise cv.Invalid('Maximum frequency for bit depth {} is {}Hz. Please decrease the '
-                         'bit_depth.'.format(bit_depth, int(math.floor(max_freq))))
-    if frequency < calc_min_frequency(bit_depth):
-        raise cv.Invalid('Minimum frequency for bit depth {} is {}Hz. Please increase the '
-                         'bit_depth.'.format(bit_depth, int(math.ceil(min_freq))))
-    return obj
+def validate_frequency(value):
+    value = cv.frequency(value)
+    min_freq = calc_min_frequency(20)
+    max_freq = calc_max_frequency(1)
+    if value < min_freq:
+        raise cv.Invalid("This frequency setting is not possible, please choose a higher "
+                         "frequency (at least {}Hz)".format(int(min_freq)))
+    if value > max_freq:
+        raise cv.Invalid("This frequency setting is not possible, please choose a lower "
+                         "frequency (at most {}Hz)".format(int(max_freq)))
+    return value
 
 
 ledc_ns = cg.esphome_ns.namespace('ledc')
 LEDCOutput = ledc_ns.class_('LEDCOutput', output.FloatOutput, cg.Component)
+SetFrequencyAction = ledc_ns.class_('SetFrequencyAction', automation.Action)
 
-CONFIG_SCHEMA = cv.All(output.FLOAT_OUTPUT_SCHEMA.extend({
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
     cv.Required(CONF_ID): cv.declare_id(LEDCOutput),
     cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
     cv.Optional(CONF_FREQUENCY, default='1kHz'): cv.frequency,
-    cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=1, max=15),
     cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15),
-}).extend(cv.COMPONENT_SCHEMA), validate_frequency_bit_depth)
+
+    cv.Optional(CONF_BIT_DEPTH): cv.invalid("The bit_depth option has been removed in v1.14, the "
+                                            "best bit depth is now automatically calculated."),
+}).extend(cv.COMPONENT_SCHEMA)
 
 
 def to_code(config):
@@ -72,4 +53,15 @@ def to_code(config):
     if CONF_CHANNEL in config:
         cg.add(var.set_channel(config[CONF_CHANNEL]))
     cg.add(var.set_frequency(config[CONF_FREQUENCY]))
-    cg.add(var.set_bit_depth(config[CONF_BIT_DEPTH]))
+
+
+@automation.register_action('output.ledc.set_frequency', SetFrequencyAction, cv.Schema({
+    cv.Required(CONF_ID): cv.use_id(LEDCOutput),
+    cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency),
+}))
+def ledc_set_frequency_to_code(config, action_id, template_arg, args):
+    paren = yield cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+    template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float)
+    cg.add(var.set_frequency(template_))
+    yield var
diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp
index c93392418c..b5dc70a083 100644
--- a/esphome/components/light/addressable_light.cpp
+++ b/esphome/components/light/addressable_light.cpp
@@ -113,7 +113,7 @@ void ESPRangeView::fade_to_white(uint8_t amnt) {
 }
 void ESPRangeView::fade_to_black(uint8_t amnt) {
   for (auto c : *this)
-    c.fade_to_white(amnt);
+    c.fade_to_black(amnt);
 }
 void ESPRangeView::lighten(uint8_t delta) {
   for (auto c : *this)
@@ -162,7 +162,6 @@ int32_t HOT interpret_index(int32_t index, int32_t size) {
 }
 
 void AddressableLight::call_setup() {
-  this->setup_internal_();
   this->setup();
 
 #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
@@ -180,5 +179,101 @@ void AddressableLight::call_setup() {
 #endif
 }
 
+ESPColor esp_color_from_light_color_values(LightColorValues val) {
+  auto r = static_cast<uint8_t>(roundf(val.get_red() * 255.0f));
+  auto g = static_cast<uint8_t>(roundf(val.get_green() * 255.0f));
+  auto b = static_cast<uint8_t>(roundf(val.get_blue() * 255.0f));
+  auto w = static_cast<uint8_t>(roundf(val.get_white() * val.get_state() * 255.0f));
+  return ESPColor(r, g, b, w);
+}
+
+void AddressableLight::write_state(LightState *state) {
+  auto val = state->current_values;
+  auto max_brightness = static_cast<uint8_t>(roundf(val.get_brightness() * val.get_state() * 255.0f));
+  this->correction_.set_local_brightness(max_brightness);
+
+  this->last_transition_progress_ = 0.0f;
+  this->accumulated_alpha_ = 0.0f;
+
+  if (this->is_effect_active())
+    return;
+
+  // don't use LightState helper, gamma correction+brightness is handled by ESPColorView
+
+  if (state->transformer_ == nullptr || !state->transformer_->is_transition()) {
+    // no transformer active or non-transition one
+    this->all() = esp_color_from_light_color_values(val);
+  } else {
+    // transition transformer active, activate specialized transition for addressable effects
+    // instead of using a unified transition for all LEDs, we use the current state each LED as the
+    // start. Warning: ugly
+
+    // We can't use a direct lerp smoothing here though - that would require creating a copy of the original
+    // state of each LED at the start of the transition
+    // Instead, we "fake" the look of the LERP by using an exponential average over time and using
+    // dynamically-calculated alpha values to match the look of the
+
+    float new_progress = state->transformer_->get_progress();
+    float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_);
+    float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress);
+    this->last_transition_progress_ = new_progress;
+
+    auto end_values = state->transformer_->get_end_values();
+    ESPColor target_color = esp_color_from_light_color_values(end_values);
+
+    // our transition will handle brightness, disable brightness in correction.
+    this->correction_.set_local_brightness(255);
+    uint8_t orig_w = target_color.w;
+    target_color *= static_cast<uint8_t>(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f));
+    // w is not scaled by brightness
+    target_color.w = orig_w;
+
+    float denom = (1.0f - new_smoothed);
+    float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom;
+
+    // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
+    // We solve this by accumulating the fractional part of the alpha over time.
+    float alpha255 = alpha * 255.0f;
+    float alpha255int = floorf(alpha255);
+    float alpha255remainder = alpha255 - alpha255int;
+
+    this->accumulated_alpha_ += alpha255remainder;
+    float alpha_add = floorf(this->accumulated_alpha_);
+    this->accumulated_alpha_ -= alpha_add;
+
+    alpha255 += alpha_add;
+    alpha255 = clamp(alpha255, 0.0f, 255.0f);
+    auto alpha8 = static_cast<uint8_t>(alpha255);
+
+    if (alpha8 != 0) {
+      uint8_t inv_alpha8 = 255 - alpha8;
+      ESPColor add = target_color * alpha8;
+
+      for (auto led : *this)
+        led = add + led.get() * inv_alpha8;
+    }
+  }
+
+  this->schedule_show();
+}
+
+void ESPColorCorrection::calculate_gamma_table(float gamma) {
+  for (uint16_t i = 0; i < 256; i++) {
+    // corrected = val ^ gamma
+    auto corrected = static_cast<uint8_t>(roundf(255.0f * gamma_correct(i / 255.0f, gamma)));
+    this->gamma_table_[i] = corrected;
+  }
+  if (gamma == 0.0f) {
+    for (uint16_t i = 0; i < 256; i++)
+      this->gamma_reverse_table_[i] = i;
+    return;
+  }
+  for (uint16_t i = 0; i < 256; i++) {
+    // val = corrected ^ (1/gamma)
+    auto uncorrected = static_cast<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma)));
+    this->gamma_reverse_table_[i] = uncorrected;
+  }
+}
+
 }  // namespace light
 }  // namespace esphome
diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h
index 4383b4b245..a95d70f274 100644
--- a/esphome/components/light/addressable_light.h
+++ b/esphome/components/light/addressable_light.h
@@ -189,23 +189,7 @@ class ESPColorCorrection {
   ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
   void set_max_brightness(const ESPColor &max_brightness) { this->max_brightness_ = max_brightness; }
   void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; }
-  void calculate_gamma_table(float gamma) {
-    for (uint16_t i = 0; i < 256; i++) {
-      // corrected = val ^ gamma
-      auto corrected = static_cast<uint8_t>(roundf(255.0f * gamma_correct(i / 255.0f, gamma)));
-      this->gamma_table_[i] = corrected;
-    }
-    if (gamma == 0.0f) {
-      for (uint16_t i = 0; i < 256; i++)
-        this->gamma_reverse_table_[i] = i;
-      return;
-    }
-    for (uint16_t i = 0; i < 256; i++) {
-      // val = corrected ^ (1/gamma)
-      auto uncorrected = static_cast<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma)));
-      this->gamma_reverse_table_[i] = uncorrected;
-    }
-  }
+  void calculate_gamma_table(float gamma);
   inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE {
     // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
     return ESPColor(this->color_correct_red(color.red), this->color_correct_green(color.green),
@@ -468,23 +452,7 @@ class AddressableLight : public LightOutput, public Component {
   }
   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 {
-    auto val = state->current_values;
-    auto max_brightness = static_cast<uint8_t>(roundf(val.get_brightness() * val.get_state() * 255.0f));
-    this->correction_.set_local_brightness(max_brightness);
-
-    if (this->is_effect_active())
-      return;
-
-    // don't use LightState helper, gamma correction+brightness is handled by ESPColorView
-    ESPColor color = ESPColor(uint8_t(roundf(val.get_red() * 255.0f)), uint8_t(roundf(val.get_green() * 255.0f)),
-                              uint8_t(roundf(val.get_blue() * 255.0f)),
-                              // white is not affected by brightness; so manually scale by state
-                              uint8_t(roundf(val.get_white() * val.get_state() * 255.0f)));
-
-    this->all() = color;
-    this->schedule_show();
-  }
+  void write_state(LightState *state) override;
   void set_correction(float red, float green, float blue, float white = 1.0f) {
     this->correction_.set_max_brightness(ESPColor(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)),
                                                   uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f))));
@@ -524,6 +492,8 @@ class AddressableLight : public LightOutput, public Component {
   power_supply::PowerSupplyRequester power_;
 #endif
   LightState *state_parent_{nullptr};
+  float last_transition_progress_{0.0f};
+  float accumulated_alpha_{0.0f};
 };
 
 }  // namespace light
diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h
index 1e0b540285..78ae41baad 100644
--- a/esphome/components/light/addressable_light_effect.h
+++ b/esphome/components/light/addressable_light_effect.h
@@ -50,19 +50,19 @@ class AddressableLightEffect : public LightEffect {
 
 class AddressableLambdaLightEffect : public AddressableLightEffect {
  public:
-  AddressableLambdaLightEffect(const std::string &name, const std::function<void(AddressableLight &)> &f,
+  AddressableLambdaLightEffect(const std::string &name, const std::function<void(AddressableLight &, ESPColor)> &f,
                                uint32_t update_interval)
       : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {}
   void apply(AddressableLight &it, const ESPColor &current_color) override {
     const uint32_t now = millis();
     if (now - this->last_run_ >= this->update_interval_) {
       this->last_run_ = now;
-      this->f_(it);
+      this->f_(it, current_color);
     }
   }
 
  protected:
-  std::function<void(AddressableLight &)> f_;
+  std::function<void(AddressableLight &, ESPColor)> f_;
   uint32_t update_interval_;
   uint32_t last_run_{0};
 };
@@ -143,14 +143,19 @@ class AddressableScanEffect : public AddressableLightEffect {
  public:
   explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {}
   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 ESPColor &current_color) override {
     it.all() = ESPColor::BLACK;
-    it[this->at_led_] = current_color;
+
+    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() - 1)
+        if (this->at_led_ == it.size() - this->scan_width_)
           this->direction_ = false;
       } else {
         this->at_led_--;
@@ -163,6 +168,7 @@ class AddressableScanEffect : public AddressableLightEffect {
 
  protected:
   uint32_t move_interval_{};
+  uint32_t scan_width_{1};
   uint32_t last_move_{0};
   int at_led_{0};
   bool direction_{true};
@@ -312,11 +318,16 @@ class AddressableFlickerEffect : public AddressableLightEffect {
     const uint8_t inv_intensity = 255 - intensity;
     if (now - this->last_update_ < this->update_interval_)
       return;
+
     this->last_update_ = now;
     fast_random_set_seed(random_uint32());
     for (auto var : it) {
       const uint8_t flicker = fast_random_8() % intensity;
-      var = (var.get() * inv_intensity) + (current_color * flicker);
+      // scale down by random factor
+      var = var.get() * (255 - flicker);
+
+      // slowly fade back to "real" value
+      var = (var.get() * inv_intensity) + (current_color * intensity);
     }
   }
   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h
index 70c423af0a..2cd55ab6f6 100644
--- a/esphome/components/light/automation.h
+++ b/esphome/components/light/automation.h
@@ -122,6 +122,7 @@ template<typename... Ts> class AddressableSet : public Action<Ts...> {
       range.set_blue(this->blue_.value(x...));
     if (this->white_.has_value())
       range.set_white(this->white_.value(x...));
+    out->schedule_show();
   }
 
  protected:
diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h
index 55aa007f56..dcef60397d 100644
--- a/esphome/components/light/base_light_effects.h
+++ b/esphome/components/light/base_light_effects.h
@@ -102,6 +102,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_white_if_supported(0.0f);
       call.set_state(true);
     }
     call.set_publish(false);
diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py
index a78165fb8a..c2250e7e0c 100644
--- a/esphome/components/light/effects.py
+++ b/esphome/components/light/effects.py
@@ -11,11 +11,12 @@ from .types import LambdaLightEffect, RandomLightEffect, StrobeLightEffect, \
     FlickerLightEffect, AddressableRainbowLightEffect, AddressableColorWipeEffect, \
     AddressableColorWipeEffectColor, AddressableScanEffect, AddressableTwinkleEffect, \
     AddressableRandomTwinkleEffect, AddressableFireworksEffect, AddressableFlickerEffect, \
-    AutomationLightEffect
+    AutomationLightEffect, ESPColor
 
 CONF_ADD_LED_INTERVAL = 'add_led_interval'
 CONF_REVERSE = 'reverse'
 CONF_MOVE_INTERVAL = 'move_interval'
+CONF_SCAN_WIDTH = 'scan_width'
 CONF_TWINKLE_PROBABILITY = 'twinkle_probability'
 CONF_PROGRESS_INTERVAL = 'progress_interval'
 CONF_SPARK_PROBABILITY = 'spark_probability'
@@ -128,7 +129,7 @@ def flicker_effect_to_code(config, effect_id):
     cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds,
 })
 def addressable_lambda_effect_to_code(config, effect_id):
-    args = [(AddressableLightRef, 'it')]
+    args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color')]
     lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void)
     var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_,
                            config[CONF_UPDATE_INTERVAL])
@@ -179,10 +180,12 @@ def addressable_color_wipe_effect_to_code(config, effect_id):
 
 @register_effect('addressable_scan', AddressableScanEffect, "Scan", {
     cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds,
+    cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1),
 })
 def addressable_scan_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL]))
+    cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH]))
     yield var
 
 
diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp
index cafced27fc..e96d64ad1f 100644
--- a/esphome/components/light/light_state.cpp
+++ b/esphome/components/light/light_state.cpp
@@ -24,9 +24,12 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
 
 LightState::LightState(const std::string &name, LightOutput *output) : Nameable(name), output_(output) {}
 
-void LightState::set_immediately_(const LightColorValues &target) {
+void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
   this->transformer_ = nullptr;
-  this->current_values = this->remote_values = target;
+  this->current_values = target;
+  if (set_remote_values) {
+    this->remote_values = target;
+  }
   this->next_write_ = true;
 }
 
@@ -327,10 +330,10 @@ void LightCall::perform() {
 
     // Also set light color values when starting an effect
     // For example to turn off the light
-    this->parent_->set_immediately_(v);
+    this->parent_->set_immediately_(v, true);
   } else {
     // INSTANT CHANGE
-    this->parent_->set_immediately_(v);
+    this->parent_->set_immediately_(v, this->publish_);
   }
 
   if (this->publish_) {
@@ -460,7 +463,8 @@ LightColorValues LightCall::validate_() {
     this->transition_length_.reset();
   }
 
-  if (!this->has_transition_() && !this->has_flash_() && !this->has_effect_() && supports_transition) {
+  if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) &&
+      supports_transition) {
     // nothing specified and light supports transitions, set default transition length
     this->transition_length_ = this->parent_->default_transition_length_;
   }
diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h
index d67aa2c53d..07a0e3147b 100644
--- a/esphome/components/light/light_state.h
+++ b/esphome/components/light/light_state.h
@@ -277,6 +277,7 @@ class LightState : public Nameable, public Component {
  protected:
   friend LightOutput;
   friend LightCall;
+  friend class AddressableLight;
 
   uint32_t hash_base() override;
 
@@ -291,7 +292,7 @@ class LightState : public Nameable, public Component {
   void start_flash_(const LightColorValues &target, uint32_t length);
 
   /// Internal method to set the color values to target immediately (with no transition).
-  void set_immediately_(const LightColorValues &target);
+  void set_immediately_(const LightColorValues &target, bool set_remote_values);
 
   /// Internal method to start a transformer.
   void set_transformer_(std::unique_ptr<LightTransformer> transformer);
diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h
index 91a406f425..222be7802c 100644
--- a/esphome/components/light/light_transformer.h
+++ b/esphome/components/light/light_transformer.h
@@ -17,7 +17,7 @@ class LightTransformer {
   LightTransformer() = delete;
 
   /// Whether this transformation is finished
-  virtual bool is_finished() { return this->get_progress_() >= 1.0f; }
+  virtual bool is_finished() { return this->get_progress() >= 1.0f; }
 
   /// This will be called to get the current values for output.
   virtual LightColorValues get_values() = 0;
@@ -29,11 +29,11 @@ class LightTransformer {
   virtual LightColorValues get_end_values() { return this->get_target_values_(); }
 
   virtual bool publish_at_end() = 0;
+  virtual bool is_transition() = 0;
+
+  float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
 
  protected:
-  /// Get the completion of this transformer, 0 to 1.
-  float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
-
   const LightColorValues &get_start_values_() const { return this->start_values_; }
 
   const LightColorValues &get_target_values_() const { return this->target_values_; }
@@ -61,12 +61,14 @@ class LightTransitionTransformer : public LightTransformer {
   }
 
   LightColorValues get_values() override {
-    float x = this->get_progress_();
-    float v = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f);
+    float v = LightTransitionTransformer::smoothed_progress(this->get_progress());
     return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v);
   }
 
   bool publish_at_end() override { return false; }
+  bool is_transition() override { return true; }
+
+  static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
 };
 
 class LightFlashTransformer : public LightTransformer {
@@ -80,6 +82,7 @@ class LightFlashTransformer : public LightTransformer {
   LightColorValues get_end_values() override { return this->get_start_values_(); }
 
   bool publish_at_end() override { return true; }
+  bool is_transition() override { return false; }
 };
 
 }  // namespace light
diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py
index 185105831d..fb88d021f2 100644
--- a/esphome/components/light/types.py
+++ b/esphome/components/light/types.py
@@ -9,6 +9,8 @@ AddressableLightState = light_ns.class_('LightState', LightState)
 LightOutput = light_ns.class_('LightOutput')
 AddressableLight = light_ns.class_('AddressableLight', cg.Component)
 AddressableLightRef = AddressableLight.operator('ref')
+
+ESPColor = light_ns.class_('ESPColor')
 LightColorValues = light_ns.class_('LightColorValues')
 
 # Actions
diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py
index 94b33ae18e..3e07334313 100644
--- a/esphome/components/logger/__init__.py
+++ b/esphome/components/logger/__init__.py
@@ -29,7 +29,7 @@ LOG_LEVEL_TO_ESP_LOG = {
     'VERY_VERBOSE': cg.global_ns.ESP_LOGVV,
 }
 
-LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE']
+LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'CONFIG', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE']
 
 UART_SELECTION_ESP32 = ['UART0', 'UART1', 'UART2']
 
@@ -123,6 +123,8 @@ def to_code(config):
             'TLS_MEM',
             'UPDATER',
             'WIFI',
+            # Spams logs too much:
+            # 'MDNS_RESPONDER',
         }
         for comp in DEBUG_COMPONENTS:
             cg.add_build_flag("-DDEBUG_ESP_{}".format(comp))
diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp
index 78f09989e4..bc6951c9b9 100644
--- a/esphome/components/logger/logger.cpp
+++ b/esphome/components/logger/logger.cpp
@@ -10,38 +10,74 @@ namespace logger {
 
 static const char *TAG = "logger";
 
-int HOT Logger::log_vprintf_(int level, const char *tag, const char *format, va_list args) {  // NOLINT
-  if (level > this->level_for(tag))
-    return 0;
+static const char *LOG_LEVEL_COLORS[] = {
+    "",                                            // NONE
+    ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED),       // ERROR
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW),   // WARNING
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN),    // INFO
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA),  // CONFIG
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN),     // DEBUG
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY),     // VERBOSE
+    ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE),    // VERY_VERBOSE
+};
+static const char *LOG_LEVEL_LETTERS[] = {
+    "",    // NONE
+    "E",   // ERROR
+    "W",   // WARNING
+    "I",   // INFO
+    "C",   // CONFIG
+    "D",   // DEBUG
+    "V",   // VERBOSE
+    "VV",  // VERY_VERBOSE
+};
 
-  int ret = vsnprintf(this->tx_buffer_.data(), this->tx_buffer_.capacity(), format, args);
-  this->log_message_(level, tag, this->tx_buffer_.data(), ret);
-  return ret;
+void Logger::write_header_(int level, const char *tag, int line) {
+  if (level < 0)
+    level = 0;
+  if (level > 7)
+    level = 7;
+
+  const char *color = LOG_LEVEL_COLORS[level];
+  const char *letter = LOG_LEVEL_LETTERS[level];
+  this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, 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))
+    return;
+
+  this->reset_buffer_();
+  this->write_header_(level, tag, line);
+  this->vprintf_to_buffer_(format, args);
+  this->write_footer_();
+  this->log_message_(level, tag);
 }
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-int Logger::log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) {  // NOLINT
+void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
+                          va_list args) {  // NOLINT
   if (level > this->level_for(tag))
-    return 0;
+    return;
 
+  this->reset_buffer_();
   // copy format string
   const char *format_pgm_p = (PGM_P) format;
   size_t len = 0;
-  char *write = this->tx_buffer_.data();
   char ch = '.';
-  while (len < this->tx_buffer_.capacity() && ch != '\0') {
-    *write++ = ch = pgm_read_byte(format_pgm_p++);
-    len++;
+  while (!this->is_buffer_full_() && ch != '\0') {
+    this->tx_buffer_[this->tx_buffer_at_++] = ch = pgm_read_byte(format_pgm_p++);
   }
-  if (len == this->tx_buffer_.capacity())
-    return -1;
+  // Buffer full form copying format
+  if (this->is_buffer_full_())
+    return;
+
+  // length of format string, includes null terminator
+  uint32_t offset = this->tx_buffer_at_;
 
   // now apply vsnprintf
-  size_t offset = len + 1;
-  size_t remaining = this->tx_buffer_.capacity() - offset;
-  char *msg = this->tx_buffer_.data() + offset;
-  int ret = vsnprintf(msg, remaining, this->tx_buffer_.data(), args);
-  this->log_message_(level, tag, msg, ret);
-  return ret;
+  this->write_header_(level, tag, line);
+  this->vprintf_to_buffer_(this->tx_buffer_, args);
+  this->write_footer_();
+  this->log_message_(level, tag, offset);
 }
 #endif
 
@@ -54,22 +90,26 @@ int HOT Logger::level_for(const char *tag) {
       return it.level;
     }
   }
-  return this->global_log_level_;
+  return ESPHOME_LOG_LEVEL;
 }
-void HOT Logger::log_message_(int level, const char *tag, char *msg, int ret) {
-  if (ret <= 0)
-    return;
+void HOT Logger::log_message_(int level, const char *tag, int offset) {
   // remove trailing newline
-  if (msg[ret - 1] == '\n') {
-    msg[ret - 1] = '\0';
+  if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') {
+    this->tx_buffer_at_--;
   }
+  // make sure null terminator is present
+  this->set_null_terminator_();
+
+  const char *msg = this->tx_buffer_ + offset;
   if (this->baud_rate_ > 0)
     this->hw_serial_->println(msg);
   this->log_callback_.call(level, tag, msg);
 }
 
-Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) : baud_rate_(baud_rate), uart_(uart) {
-  this->set_tx_buffer_size(tx_buffer_size);
+Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart)
+    : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) {
+  // add 1 to buffer size for null terminator
+  this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];
 }
 
 void Logger::pre_setup() {
@@ -96,7 +136,7 @@ void Logger::pre_setup() {
     if (this->uart_ == UART_SELECTION_UART0_SWAP) {
       this->hw_serial_->swap();
     }
-    this->hw_serial_->setDebugOutput(this->global_log_level_ >= ESPHOME_LOG_LEVEL_VERBOSE);
+    this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
 #endif
   }
 #ifdef ARDUINO_ARCH_ESP8266
@@ -108,7 +148,7 @@ void Logger::pre_setup() {
   global_logger = this;
 #ifdef ARDUINO_ARCH_ESP32
   esp_log_set_vprintf(esp_idf_log_vprintf_);
-  if (this->global_log_level_ >= ESPHOME_LOG_LEVEL_VERBOSE) {
+  if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
     esp_log_level_set("*", ESP_LOG_VERBOSE);
   }
 #endif
@@ -116,17 +156,15 @@ void Logger::pre_setup() {
   ESP_LOGI(TAG, "Log initialized");
 }
 void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
-void Logger::set_global_log_level(int log_level) { this->global_log_level_ = log_level; }
 void Logger::set_log_level(const std::string &tag, int log_level) {
   this->log_levels_.push_back(LogLevelOverride{tag, log_level});
 }
-void Logger::set_tx_buffer_size(size_t tx_buffer_size) { this->tx_buffer_.reserve(tx_buffer_size); }
 UARTSelection Logger::get_uart() const { return this->uart_; }
 void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) {
   this->log_callback_.add(std::move(callback));
 }
 float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
-const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
+const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
 #ifdef ARDUINO_ARCH_ESP32
 const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART2"};
 #endif
@@ -135,13 +173,14 @@ const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
 #endif
 void Logger::dump_config() {
   ESP_LOGCONFIG(TAG, "Logger:");
-  ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[this->global_log_level_]);
+  ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
   ESP_LOGCONFIG(TAG, "  Log Baud Rate: %u", this->baud_rate_);
   ESP_LOGCONFIG(TAG, "  Hardware UART: %s", UART_SELECTIONS[this->uart_]);
   for (auto &it : this->log_levels_) {
     ESP_LOGCONFIG(TAG, "  Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]);
   }
 }
+void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); }
 
 Logger *global_logger = nullptr;
 
diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h
index 6f06c63595..b8a252c7bd 100644
--- a/esphome/components/logger/logger.h
+++ b/esphome/components/logger/logger.h
@@ -30,17 +30,11 @@ class Logger : public Component {
 
   /// Manually set the baud rate for serial, set to 0 to disable.
   void set_baud_rate(uint32_t baud_rate);
-
-  /// Set the buffer size that's used for constructing log messages. Log messages longer than this will be truncated.
-  void set_tx_buffer_size(size_t tx_buffer_size);
+  uint32_t get_baud_rate() const { return baud_rate_; }
 
   /// Get the UART used by the logger.
   UARTSelection get_uart() const;
 
-  /// Set the global log level. Note: Use the ESPHOME_LOG_LEVEL define to also remove the logs from the build.
-  void set_global_log_level(int log_level);
-  int get_global_log_level() const { return this->global_log_level_; }
-
   /// Set the log level of the specified tag.
   void set_log_level(const std::string &tag, int log_level);
 
@@ -57,17 +51,58 @@ class Logger : public Component {
 
   float get_setup_priority() const override;
 
-  int log_vprintf_(int level, const char *tag, const char *format, va_list args);  // NOLINT
+  void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args);  // NOLINT
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-  int log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args);  // NOLINT
+  void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args);  // NOLINT
 #endif
 
  protected:
-  void log_message_(int level, const char *tag, char *msg, int ret);
+  void write_header_(int level, const char *tag, int line);
+  void write_footer_();
+  void log_message_(int level, const char *tag, int offset = 0);
+
+  inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; }
+  inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; }
+  inline void reset_buffer_() { this->tx_buffer_at_ = 0; }
+  inline void set_null_terminator_() {
+    // does not increment buffer_at
+    this->tx_buffer_[this->tx_buffer_at_] = '\0';
+  }
+  inline void write_to_buffer_(char value) {
+    if (!this->is_buffer_full_())
+      this->tx_buffer_[this->tx_buffer_at_++] = value;
+  }
+  inline void write_to_buffer_(const char *value, int length) {
+    for (int i = 0; i < length && !this->is_buffer_full_(); i++) {
+      this->tx_buffer_[this->tx_buffer_at_++] = value[i];
+    }
+  }
+  inline void vprintf_to_buffer_(const char *format, va_list args) {
+    if (this->is_buffer_full_())
+      return;
+    int remaining = this->buffer_remaining_capacity_();
+    int ret = vsnprintf(this->tx_buffer_ + this->tx_buffer_at_, remaining, format, args);
+    if (ret < 0) {
+      // Encoding error, do not increment buffer_at
+      return;
+    }
+    if (ret >= remaining) {
+      // output was too long, truncated
+      ret = remaining;
+    }
+    this->tx_buffer_at_ += ret;
+  }
+  inline void printf_to_buffer_(const char *format, ...) {
+    va_list arg;
+    va_start(arg, format);
+    this->vprintf_to_buffer_(format, arg);
+    va_end(arg);
+  }
 
   uint32_t baud_rate_;
-  std::vector<char> tx_buffer_;
-  int global_log_level_{ESPHOME_LOG_LEVEL};
+  char *tx_buffer_{nullptr};
+  int tx_buffer_at_{0};
+  int tx_buffer_size_{0};
   UARTSelection uart_{UART_SELECTION_UART0};
   HardwareSerial *hw_serial_{nullptr};
   struct LogLevelOverride {
diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp
index 18a00b10d7..0462ed4342 100644
--- a/esphome/components/max31855/max31855.cpp
+++ b/esphome/components/max31855/max31855.cpp
@@ -82,7 +82,5 @@ void MAX31855Sensor::read_data_() {
   this->status_clear_warning();
 }
 
-bool MAX31855Sensor::is_device_msb_first() { return true; }
-
 }  // namespace max31855
 }  // namespace esphome
diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h
index f9cdf335f1..1d0fc79ac0 100644
--- a/esphome/components/max31855/max31855.h
+++ b/esphome/components/max31855/max31855.h
@@ -7,7 +7,10 @@
 namespace esphome {
 namespace max31855 {
 
-class MAX31855Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice {
+class MAX31855Sensor : public sensor::Sensor,
+                       public PollingComponent,
+                       public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                             spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_4MHZ> {
  public:
   void setup() override;
   void dump_config() override;
@@ -16,8 +19,6 @@ class MAX31855Sensor : public sensor::Sensor, public PollingComponent, public sp
   void update() override;
 
  protected:
-  bool is_device_msb_first() override;
-
   void read_data_();
 };
 
diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp
index 8ea7feb963..53442b9cb1 100644
--- a/esphome/components/max6675/max6675.cpp
+++ b/esphome/components/max6675/max6675.cpp
@@ -48,7 +48,5 @@ void MAX6675Sensor::read_data_() {
   this->status_clear_warning();
 }
 
-bool MAX6675Sensor::is_device_msb_first() { return true; }
-
 }  // namespace max6675
 }  // namespace esphome
diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h
index 48f51fbe11..09bd9df3b8 100644
--- a/esphome/components/max6675/max6675.h
+++ b/esphome/components/max6675/max6675.h
@@ -7,7 +7,10 @@
 namespace esphome {
 namespace max6675 {
 
-class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice {
+class MAX6675Sensor : public sensor::Sensor,
+                      public PollingComponent,
+                      public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
+                                            spi::DATA_RATE_1KHZ> {
  public:
   void setup() override;
   void dump_config() override;
@@ -16,8 +19,6 @@ class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi
   void update() override;
 
  protected:
-  bool is_device_msb_first() override;
-
   void read_data_();
 };
 
diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp
index bc3c3ae0c9..db43ff19f6 100644
--- a/esphome/components/max7219/max7219.cpp
+++ b/esphome/components/max7219/max7219.cpp
@@ -155,7 +155,6 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) {
     this->send_byte_(a_register, data);
   this->disable();
 }
-bool MAX7219Component::is_device_msb_first() { return true; }
 void MAX7219Component::update() {
   for (uint8_t i = 0; i < this->num_chips_ * 8; i++)
     this->buffer_[i] = 0;
diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h
index e2379fa69b..1920268ba4 100644
--- a/esphome/components/max7219/max7219.h
+++ b/esphome/components/max7219/max7219.h
@@ -16,7 +16,9 @@ class MAX7219Component;
 
 using max7219_writer_t = std::function<void(MAX7219Component &)>;
 
-class MAX7219Component : public PollingComponent, public spi::SPIDevice {
+class MAX7219Component : public PollingComponent,
+                         public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                               spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
  public:
   void set_writer(max7219_writer_t &&writer);
 
@@ -54,7 +56,6 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice {
  protected:
   void send_byte_(uint8_t a_register, uint8_t data);
   void send_to_all_(uint8_t a_register, uint8_t data);
-  bool is_device_msb_first() override;
 
   uint8_t intensity_{15};  /// Intensity of the display from 0 to 15 (most)
   uint8_t num_chips_{1};
diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py
new file mode 100644
index 0000000000..4241b6ba48
--- /dev/null
+++ b/esphome/components/mcp23008/__init__.py
@@ -0,0 +1,51 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import i2c
+from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED
+
+DEPENDENCIES = ['i2c']
+MULTI_CONF = True
+
+mcp23008_ns = cg.esphome_ns.namespace('mcp23008')
+MCP23008GPIOMode = mcp23008_ns.enum('MCP23008GPIOMode')
+MCP23008_GPIO_MODES = {
+    'INPUT': MCP23008GPIOMode.MCP23008_INPUT,
+    'INPUT_PULLUP': MCP23008GPIOMode.MCP23008_INPUT_PULLUP,
+    'OUTPUT': MCP23008GPIOMode.MCP23008_OUTPUT,
+}
+
+MCP23008 = mcp23008_ns.class_('MCP23008', cg.Component, i2c.I2CDevice)
+MCP23008GPIOPin = mcp23008_ns.class_('MCP23008GPIOPin', cg.GPIOPin)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.Required(CONF_ID): cv.declare_id(MCP23008),
+}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+
+
+CONF_MCP23008 = 'mcp23008'
+MCP23008_OUTPUT_PIN_SCHEMA = cv.Schema({
+    cv.Required(CONF_MCP23008): cv.use_id(MCP23008),
+    cv.Required(CONF_NUMBER): cv.int_,
+    cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True),
+    cv.Optional(CONF_INVERTED, default=False): cv.boolean,
+})
+MCP23008_INPUT_PIN_SCHEMA = cv.Schema({
+    cv.Required(CONF_MCP23008): cv.use_id(MCP23008),
+    cv.Required(CONF_NUMBER): cv.int_,
+    cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True),
+    cv.Optional(CONF_INVERTED, default=False): cv.boolean,
+})
+
+
+@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23008,
+                                   (MCP23008_OUTPUT_PIN_SCHEMA, MCP23008_INPUT_PIN_SCHEMA))
+def mcp23008_pin_to_code(config):
+    parent = yield cg.get_variable(config[CONF_MCP23008])
+    yield MCP23008GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED])
diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp
new file mode 100644
index 0000000000..bf5bb55f2e
--- /dev/null
+++ b/esphome/components/mcp23008/mcp23008.cpp
@@ -0,0 +1,91 @@
+#include "mcp23008.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace mcp23008 {
+
+static const char *TAG = "mcp23008";
+
+void MCP23008::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up MCP23008...");
+  uint8_t iocon;
+  if (!this->read_reg_(MCP23008_IOCON, &iocon)) {
+    this->mark_failed();
+    return;
+  }
+
+  // all pins input
+  this->write_reg_(MCP23008_IODIR, 0xFF);
+}
+bool MCP23008::digital_read(uint8_t pin) {
+  uint8_t bit = pin % 8;
+  uint8_t reg_addr = MCP23008_GPIO;
+  uint8_t value = 0;
+  this->read_reg_(reg_addr, &value);
+  return value & (1 << bit);
+}
+void MCP23008::digital_write(uint8_t pin, bool value) {
+  uint8_t reg_addr = MCP23008_OLAT;
+  this->update_reg_(pin, value, reg_addr);
+}
+void MCP23008::pin_mode(uint8_t pin, uint8_t mode) {
+  uint8_t iodir = MCP23008_IODIR;
+  uint8_t gppu = MCP23008_GPPU;
+  switch (mode) {
+    case MCP23008_INPUT:
+      this->update_reg_(pin, true, iodir);
+      break;
+    case MCP23008_INPUT_PULLUP:
+      this->update_reg_(pin, true, iodir);
+      this->update_reg_(pin, true, gppu);
+      break;
+    case MCP23008_OUTPUT:
+      this->update_reg_(pin, false, iodir);
+      break;
+    default:
+      break;
+  }
+}
+float MCP23008::get_setup_priority() const { return setup_priority::HARDWARE; }
+bool MCP23008::read_reg_(uint8_t reg, uint8_t *value) {
+  if (this->is_failed())
+    return false;
+
+  return this->read_byte(reg, value);
+}
+bool MCP23008::write_reg_(uint8_t reg, uint8_t value) {
+  if (this->is_failed())
+    return false;
+
+  return this->write_byte(reg, value);
+}
+void MCP23008::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
+  uint8_t bit = pin % 8;
+  uint8_t reg_value = 0;
+  if (reg_addr == MCP23008_OLAT) {
+    reg_value = this->olat_;
+  } else {
+    this->read_reg_(reg_addr, &reg_value);
+  }
+
+  if (pin_value)
+    reg_value |= 1 << bit;
+  else
+    reg_value &= ~(1 << bit);
+
+  this->write_reg_(reg_addr, reg_value);
+
+  if (reg_addr == MCP23008_OLAT) {
+    this->olat_ = reg_value;
+  }
+}
+
+MCP23008GPIOPin::MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted)
+    : GPIOPin(pin, mode, inverted), parent_(parent) {}
+void MCP23008GPIOPin::setup() { this->pin_mode(this->mode_); }
+void MCP23008GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); }
+bool MCP23008GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
+void MCP23008GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
+
+}  // namespace mcp23008
+}  // namespace esphome
diff --git a/esphome/components/mcp23008/mcp23008.h b/esphome/components/mcp23008/mcp23008.h
new file mode 100644
index 0000000000..b4e5d75fd4
--- /dev/null
+++ b/esphome/components/mcp23008/mcp23008.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/esphal.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace mcp23008 {
+
+/// Modes for MCP23008 pins
+enum MCP23008GPIOMode : uint8_t {
+  MCP23008_INPUT = INPUT,                // 0x00
+  MCP23008_INPUT_PULLUP = INPUT_PULLUP,  // 0x02
+  MCP23008_OUTPUT = OUTPUT               // 0x01
+};
+
+enum MCP23008GPIORegisters {
+  // A side
+  MCP23008_IODIR = 0x00,
+  MCP23008_IPOL = 0x01,
+  MCP23008_GPINTEN = 0x02,
+  MCP23008_DEFVAL = 0x03,
+  MCP23008_INTCON = 0x04,
+  MCP23008_IOCON = 0x05,
+  MCP23008_GPPU = 0x06,
+  MCP23008_INTF = 0x07,
+  MCP23008_INTCAP = 0x08,
+  MCP23008_GPIO = 0x09,
+  MCP23008_OLAT = 0x0A,
+};
+
+class MCP23008 : public Component, public i2c::I2CDevice {
+ public:
+  MCP23008() = default;
+
+  void setup() override;
+
+  bool digital_read(uint8_t pin);
+  void digital_write(uint8_t pin, bool value);
+  void pin_mode(uint8_t pin, uint8_t mode);
+
+  float get_setup_priority() const override;
+
+ protected:
+  // read a given register
+  bool read_reg_(uint8_t reg, uint8_t *value);
+  // write a value to a given register
+  bool write_reg_(uint8_t reg, uint8_t value);
+  // update registers with given pin value.
+  void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a);
+
+  uint8_t olat_{0x00};
+};
+
+class MCP23008GPIOPin : public GPIOPin {
+ public:
+  MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted = false);
+
+  void setup() override;
+  void pin_mode(uint8_t mode) override;
+  bool digital_read() override;
+  void digital_write(bool value) override;
+
+ protected:
+  MCP23008 *parent_;
+};
+
+}  // namespace mcp23008
+}  // namespace esphome
diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp
index 687c816179..9653aa680d 100644
--- a/esphome/components/mcp23017/mcp23017.cpp
+++ b/esphome/components/mcp23017/mcp23017.cpp
@@ -47,7 +47,7 @@ void MCP23017::pin_mode(uint8_t pin, uint8_t mode) {
       break;
   }
 }
-float MCP23017::get_setup_priority() const { return setup_priority::HARDWARE; }
+float MCP23017::get_setup_priority() const { return setup_priority::IO; }
 bool MCP23017::read_reg_(uint8_t reg, uint8_t *value) {
   if (this->is_failed())
     return false;
diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp
index 8f46e288b6..8e28d04dea 100644
--- a/esphome/components/mhz19/mhz19.cpp
+++ b/esphome/components/mhz19/mhz19.cpp
@@ -8,6 +8,9 @@ static const char *TAG = "mhz19";
 static const uint8_t MHZ19_REQUEST_LENGTH = 8;
 static const uint8_t MHZ19_RESPONSE_LENGTH = 9;
 static const uint8_t MHZ19_COMMAND_GET_PPM[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00};
+static const uint8_t MHZ19_COMMAND_ABC_ENABLE[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00};
+static const uint8_t MHZ19_COMMAND_ABC_DISABLE[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00};
+static const uint8_t MHZ19_COMMAND_CALIBRATE_ZERO[] = {0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00};
 
 uint8_t mhz19_checksum(const uint8_t *command) {
   uint8_t sum = 0;
@@ -17,6 +20,14 @@ uint8_t mhz19_checksum(const uint8_t *command) {
   return 0xFF - sum + 0x01;
 }
 
+void MHZ19Component::setup() {
+  if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
+    this->abc_enable();
+  } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
+    this->abc_disable();
+  }
+}
+
 void MHZ19Component::update() {
   uint8_t response[MHZ19_RESPONSE_LENGTH];
   if (!this->mhz19_write_command_(MHZ19_COMMAND_GET_PPM, response)) {
@@ -50,23 +61,46 @@ void MHZ19Component::update() {
     this->temperature_sensor_->publish_state(temp);
 }
 
+void MHZ19Component::calibrate_zero() {
+  ESP_LOGD(TAG, "MHZ19 Calibrating zero point");
+  this->mhz19_write_command_(MHZ19_COMMAND_CALIBRATE_ZERO, nullptr);
+}
+
+void MHZ19Component::abc_enable() {
+  ESP_LOGD(TAG, "MHZ19 Enabling automatic baseline calibration");
+  this->mhz19_write_command_(MHZ19_COMMAND_ABC_ENABLE, nullptr);
+}
+
+void MHZ19Component::abc_disable() {
+  ESP_LOGD(TAG, "MHZ19 Disabling automatic baseline calibration");
+  this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr);
+}
+
 bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) {
-  this->flush();
+  // Empty RX Buffer
+  while (this->available())
+    this->read();
   this->write_array(command, MHZ19_REQUEST_LENGTH);
   this->write_byte(mhz19_checksum(command));
+  this->flush();
 
   if (response == nullptr)
     return true;
 
-  bool ret = this->read_array(response, MHZ19_RESPONSE_LENGTH);
-  this->flush();
-  return ret;
+  return this->read_array(response, MHZ19_RESPONSE_LENGTH);
 }
 float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; }
 void MHZ19Component::dump_config() {
   ESP_LOGCONFIG(TAG, "MH-Z19:");
   LOG_SENSOR("  ", "CO2", this->co2_sensor_);
   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  this->check_uart_settings(9600);
+
+  if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
+    ESP_LOGCONFIG(TAG, "  Automatic baseline calibration enabled on boot");
+  } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
+    ESP_LOGCONFIG(TAG, "  Automatic baseline calibration disabled on boot");
+  }
 }
 
 }  // namespace mhz19
diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h
index 3604628afc..2201fc87f0 100644
--- a/esphome/components/mhz19/mhz19.h
+++ b/esphome/components/mhz19/mhz19.h
@@ -1,27 +1,64 @@
 #pragma once
 
 #include "esphome/core/component.h"
+#include "esphome/core/automation.h"
 #include "esphome/components/sensor/sensor.h"
 #include "esphome/components/uart/uart.h"
 
 namespace esphome {
 namespace mhz19 {
 
+enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, MHZ19_ABC_ENABLED, MHZ19_ABC_DISABLED };
+
 class MHZ19Component : public PollingComponent, public uart::UARTDevice {
  public:
   float get_setup_priority() const override;
 
+  void setup() override;
   void update() override;
   void dump_config() override;
 
+  void calibrate_zero();
+  void abc_enable();
+  void abc_disable();
+
   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
   void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
+  void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; }
 
  protected:
   bool mhz19_write_command_(const uint8_t *command, uint8_t *response);
 
   sensor::Sensor *temperature_sensor_{nullptr};
   sensor::Sensor *co2_sensor_{nullptr};
+  MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE};
+};
+
+template<typename... Ts> class MHZ19CalibrateZeroAction : public Action<Ts...> {
+ public:
+  MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {}
+  void play(Ts... x) override { this->mhz19_->calibrate_zero(); }
+
+ protected:
+  MHZ19Component *mhz19_;
+};
+
+template<typename... Ts> class MHZ19ABCEnableAction : public Action<Ts...> {
+ public:
+  MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {}
+  void play(Ts... x) override { this->mhz19_->abc_enable(); }
+
+ protected:
+  MHZ19Component *mhz19_;
+};
+
+template<typename... Ts> class MHZ19ABCDisableAction : public Action<Ts...> {
+ public:
+  MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {}
+  void play(Ts... x) override { this->mhz19_->abc_disable(); }
+
+ protected:
+  MHZ19Component *mhz19_;
 };
 
 }  // namespace mhz19
diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py
index 368426e6f7..bdcecf12cb 100644
--- a/esphome/components/mhz19/sensor.py
+++ b/esphome/components/mhz19/sensor.py
@@ -1,18 +1,26 @@
 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 sensor, uart
 from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \
     UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER
 
 DEPENDENCIES = ['uart']
 
+CONF_AUTOMATIC_BASELINE_CALIBRATION = 'automatic_baseline_calibration'
+
 mhz19_ns = cg.esphome_ns.namespace('mhz19')
 MHZ19Component = mhz19_ns.class_('MHZ19Component', cg.PollingComponent, uart.UARTDevice)
+MHZ19CalibrateZeroAction = mhz19_ns.class_('MHZ19CalibrateZeroAction', automation.Action)
+MHZ19ABCEnableAction = mhz19_ns.class_('MHZ19ABCEnableAction', automation.Action)
+MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Action)
 
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.declare_id(MHZ19Component),
     cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0),
     cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0),
+    cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean,
 }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA)
 
 
@@ -28,3 +36,22 @@ def to_code(config):
     if CONF_TEMPERATURE in config:
         sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
+
+    if CONF_AUTOMATIC_BASELINE_CALIBRATION in config:
+        cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION]))
+
+
+CALIBRATION_ACTION_SCHEMA = maybe_simple_id({
+    cv.Required(CONF_ID): cv.use_id(MHZ19Component),
+})
+
+
+@automation.register_action('mhz19.calibrate_zero', MHZ19CalibrateZeroAction,
+                            CALIBRATION_ACTION_SCHEMA)
+@automation.register_action('mhz19.abc_enable', MHZ19ABCEnableAction,
+                            CALIBRATION_ACTION_SCHEMA)
+@automation.register_action('mhz19.abc_disable', MHZ19ABCDisableAction,
+                            CALIBRATION_ACTION_SCHEMA)
+def mhz19_calibration_to_code(config, action_id, template_arg, args):
+    paren = yield cg.get_variable(config[CONF_ID])
+    yield cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py
new file mode 100644
index 0000000000..cada835905
--- /dev/null
+++ b/esphome/components/modbus/__init__.py
@@ -0,0 +1,44 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import uart
+from esphome.const import CONF_ID, CONF_ADDRESS
+from esphome.core import coroutine
+
+DEPENDENCIES = ['uart']
+
+modbus_ns = cg.esphome_ns.namespace('modbus')
+Modbus = modbus_ns.class_('Modbus', cg.Component, uart.UARTDevice)
+ModbusDevice = modbus_ns.class_('ModbusDevice')
+MULTI_CONF = True
+
+CONF_MODBUS_ID = 'modbus_id'
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(Modbus),
+}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA)
+
+
+def to_code(config):
+    cg.add_global(modbus_ns.using)
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+
+    yield uart.register_uart_device(var, config)
+
+
+def modbus_device_schema(default_address):
+    schema = {
+        cv.GenerateID(CONF_MODBUS_ID): cv.use_id(Modbus),
+    }
+    if default_address is None:
+        schema[cv.Required(CONF_ADDRESS)] = cv.hex_uint8_t
+    else:
+        schema[cv.Optional(CONF_ADDRESS, default=default_address)] = cv.hex_uint8_t
+    return cv.Schema(schema)
+
+
+@coroutine
+def register_modbus_device(var, config):
+    parent = yield cg.get_variable(config[CONF_MODBUS_ID])
+    cg.add(var.set_parent(parent))
+    cg.add(var.set_address(config[CONF_ADDRESS]))
+    cg.add(parent.register_device(var))
diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
new file mode 100644
index 0000000000..74d0c40986
--- /dev/null
+++ b/esphome/components/modbus/modbus.cpp
@@ -0,0 +1,119 @@
+#include "modbus.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace modbus {
+
+static const char *TAG = "modbus";
+
+void Modbus::loop() {
+  const uint32_t now = millis();
+  if (now - this->last_modbus_byte_ > 50) {
+    this->rx_buffer_.clear();
+    this->last_modbus_byte_ = now;
+  }
+
+  while (this->available()) {
+    uint8_t byte;
+    this->read_byte(&byte);
+    if (this->parse_modbus_byte_(byte)) {
+      this->last_modbus_byte_ = now;
+    } else {
+      this->rx_buffer_.clear();
+    }
+  }
+}
+
+uint16_t crc16(const uint8_t *data, uint8_t len) {
+  uint16_t crc = 0xFFFF;
+  while (len--) {
+    crc ^= *data++;
+    for (uint8_t i = 0; i < 8; i++) {
+      if ((crc & 0x01) != 0) {
+        crc >>= 1;
+        crc ^= 0xA001;
+      } else {
+        crc >>= 1;
+      }
+    }
+  }
+  return crc;
+}
+
+bool Modbus::parse_modbus_byte_(uint8_t byte) {
+  size_t at = this->rx_buffer_.size();
+  this->rx_buffer_.push_back(byte);
+  const uint8_t *raw = &this->rx_buffer_[0];
+
+  // Byte 0: modbus address (match all)
+  if (at == 0)
+    return true;
+  uint8_t address = raw[0];
+
+  // Byte 1: Function (msb indicates error)
+  if (at == 1)
+    return (byte & 0x80) != 0x80;
+
+  // Byte 2: Size (with modbus rtu function code 4/3)
+  // See also https://en.wikipedia.org/wiki/Modbus
+  if (at == 2)
+    return true;
+
+  uint8_t data_len = raw[2];
+  // Byte 3..3+data_len-1: Data
+  if (at < 3 + data_len)
+    return true;
+
+  // Byte 3+data_len: CRC_LO (over all bytes)
+  if (at == 3 + data_len)
+    return true;
+  // Byte 3+len+1: CRC_HI (over all bytes)
+  uint16_t computed_crc = crc16(raw, 3 + data_len);
+  uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len + 1]) << 8);
+  if (computed_crc != remote_crc) {
+    ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
+    return false;
+  }
+
+  std::vector<uint8_t> data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len);
+
+  bool found = false;
+  for (auto *device : this->devices_) {
+    if (device->address_ == address) {
+      device->on_modbus_data(data);
+      found = true;
+    }
+  }
+  if (!found) {
+    ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address);
+  }
+
+  // return false to reset buffer
+  return false;
+}
+
+void Modbus::dump_config() {
+  ESP_LOGCONFIG(TAG, "Modbus:");
+  this->check_uart_settings(9600, 2);
+}
+float Modbus::get_setup_priority() const {
+  // After UART bus
+  return setup_priority::BUS - 1.0f;
+}
+void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) {
+  uint8_t frame[8];
+  frame[0] = address;
+  frame[1] = function;
+  frame[2] = start_address >> 8;
+  frame[3] = start_address >> 0;
+  frame[4] = register_count >> 8;
+  frame[5] = register_count >> 0;
+  auto crc = crc16(frame, 6);
+  frame[6] = crc >> 0;
+  frame[7] = crc >> 8;
+
+  this->write_array(frame, 8);
+}
+
+}  // namespace modbus
+}  // namespace esphome
diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h
new file mode 100644
index 0000000000..b75de147b1
--- /dev/null
+++ b/esphome/components/modbus/modbus.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/uart/uart.h"
+
+namespace esphome {
+namespace modbus {
+
+class ModbusDevice;
+
+class Modbus : public uart::UARTDevice, public Component {
+ public:
+  Modbus() = default;
+
+  void loop() override;
+
+  void dump_config() override;
+
+  void register_device(ModbusDevice *device) { this->devices_.push_back(device); }
+
+  float get_setup_priority() const override;
+
+  void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count);
+
+ protected:
+  bool parse_modbus_byte_(uint8_t byte);
+
+  std::vector<uint8_t> rx_buffer_;
+  uint32_t last_modbus_byte_{0};
+  std::vector<ModbusDevice *> devices_;
+};
+
+class ModbusDevice {
+ public:
+  void set_parent(Modbus *parent) { parent_ = parent; }
+  void set_address(uint8_t address) { address_ = address; }
+  virtual void on_modbus_data(const std::vector<uint8_t> &data) = 0;
+
+  void send(uint8_t function, uint16_t start_address, uint16_t register_count) {
+    this->parent_->send(this->address_, function, start_address, register_count);
+  }
+
+ protected:
+  friend Modbus;
+
+  Modbus *parent_;
+  uint8_t address_;
+};
+
+}  // namespace modbus
+}  // namespace esphome
diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp
index a24a703306..2025bc5b3f 100644
--- a/esphome/components/mpr121/mpr121.cpp
+++ b/esphome/components/mpr121/mpr121.cpp
@@ -10,7 +10,7 @@ void MPR121Component::setup() {
   ESP_LOGCONFIG(TAG, "Setting up MPR121...");
   // soft reset device
   this->write_byte(MPR121_SOFTRESET, 0x63);
-  delay(100);
+  delay(100);  // NOLINT
   if (!this->write_byte(MPR121_ECR, 0x0)) {
     this->error_code_ = COMMUNICATION_FAILED;
     this->mark_failed();
diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index d30e25c187..073bb3cede 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -15,7 +15,7 @@ from esphome.const import CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, CO
 from esphome.core import coroutine_with_priority, coroutine, CORE
 
 DEPENDENCIES = ['network']
-AUTO_LOAD = ['json']
+AUTO_LOAD = ['json', 'async_tcp']
 
 
 def validate_message_just_topic(value):
@@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({
     cv.Optional(CONF_SSL_FINGERPRINTS): cv.All(cv.only_on_esp8266,
                                                cv.ensure_list(validate_fingerprint)),
     cv.Optional(CONF_KEEPALIVE, default='15s'): cv.positive_time_period_seconds,
-    cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds,
+    cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds,
     cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({
         cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger),
         cv.Required(CONF_TOPIC): cv.subscribe_topic,
@@ -154,7 +154,8 @@ def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     yield cg.register_component(var, config)
 
-    cg.add_library('AsyncMqttClient', '0.8.2')
+    # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json
+    cg.add_library('AsyncMqttClient-esphome', '0.8.3')
     cg.add_define('USE_MQTT')
     cg.add_global(mqtt_ns.using)
 
diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp
index e07204d559..2eb1c52153 100644
--- a/esphome/components/mqtt/mqtt_client.cpp
+++ b/esphome/components/mqtt/mqtt_client.cpp
@@ -201,7 +201,7 @@ void MQTTClientComponent::check_connected() {
   this->status_clear_warning();
   ESP_LOGI(TAG, "MQTT Connected!");
   // MQTT Client needs some time to be fully set up.
-  delay(100);
+  delay(100);  // NOLINT
 
   this->resubscribe_subscriptions_();
 
diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp
index 8085fbf0f2..48b470cfb2 100644
--- a/esphome/components/mqtt/mqtt_climate.cpp
+++ b/esphome/components/mqtt/mqtt_climate.cpp
@@ -141,7 +141,21 @@ std::string MQTTClimateComponent::friendly_name() const { return this->device_->
 bool MQTTClimateComponent::publish_state_() {
   auto traits = this->device_->get_traits();
   // mode
-  const char *mode_s = climate_mode_to_string(this->device_->mode);
+  const char *mode_s = "";
+  switch (this->device_->mode) {
+    case CLIMATE_MODE_OFF:
+      mode_s = "off";
+      break;
+    case CLIMATE_MODE_AUTO:
+      mode_s = "auto";
+      break;
+    case CLIMATE_MODE_COOL:
+      mode_s = "cool";
+      break;
+    case CLIMATE_MODE_HEAT:
+      mode_s = "heat";
+      break;
+  }
   bool success = true;
   if (!this->publish(this->get_mode_state_topic(), mode_s))
     success = false;
diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp
index 53c4e89e98..4201d41c44 100644
--- a/esphome/components/mqtt/mqtt_component.cpp
+++ b/esphome/components/mqtt/mqtt_component.cpp
@@ -149,9 +149,6 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
 }
 void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
 void MQTTComponent::call_setup() {
-  // Call component internal setup.
-  this->setup_internal_();
-
   if (this->is_internal())
     return;
 
@@ -173,8 +170,6 @@ void MQTTComponent::call_setup() {
 }
 
 void MQTTComponent::call_loop() {
-  this->loop_internal_();
-
   if (this->is_internal())
     return;
 
diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp
index 56d18a3d22..a414c261f0 100644
--- a/esphome/components/mqtt/mqtt_cover.cpp
+++ b/esphome/components/mqtt/mqtt_cover.cpp
@@ -73,6 +73,9 @@ void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCon
     root["tilt_status_topic"] = this->get_tilt_state_topic();
     root["tilt_command_topic"] = this->get_tilt_command_topic();
   }
+  if (traits.get_supports_tilt() && !traits.get_supports_position()) {
+    config.command_topic = false;
+  }
 }
 
 std::string MQTTCoverComponent::component_type() const { return "cover"; }
diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp
index a241cf6ed6..f87e7651b9 100644
--- a/esphome/components/mqtt/mqtt_sensor.cpp
+++ b/esphome/components/mqtt/mqtt_sensor.cpp
@@ -55,6 +55,9 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo
   if (!this->sensor_->get_icon().empty())
     root["icon"] = this->sensor_->get_icon();
 
+  if (this->sensor_->get_force_update())
+    root["force_update"] = true;
+
   config.command_topic = false;
 }
 bool MQTTSensorComponent::send_initial_state() {
diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp
index e4c08c8e4e..37d475d25d 100644
--- a/esphome/components/mqtt/mqtt_text_sensor.cpp
+++ b/esphome/components/mqtt/mqtt_text_sensor.cpp
@@ -15,9 +15,6 @@ void MQTTTextSensor::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig
   if (!this->sensor_->get_icon().empty())
     root["icon"] = this->sensor_->get_icon();
 
-  if (!this->sensor_->unique_id().empty())
-    root["unique_id"] = this->sensor_->unique_id();
-
   config.command_topic = false;
 }
 void MQTTTextSensor::setup() {
@@ -40,6 +37,7 @@ bool MQTTTextSensor::send_initial_state() {
 bool MQTTTextSensor::is_internal() { return this->sensor_->is_internal(); }
 std::string MQTTTextSensor::component_type() const { return "sensor"; }
 std::string MQTTTextSensor::friendly_name() const { return this->sensor_->get_name(); }
+std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); }
 
 }  // namespace mqtt
 }  // namespace esphome
diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h
index 94afe30381..a5ce0658c7 100644
--- a/esphome/components/mqtt/mqtt_text_sensor.h
+++ b/esphome/components/mqtt/mqtt_text_sensor.h
@@ -31,6 +31,8 @@ class MQTTTextSensor : public mqtt::MQTTComponent {
 
   std::string friendly_name() const override;
 
+  std::string unique_id() override;
+
   text_sensor::TextSensor *sensor_;
 };
 
diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp
index 33ed6b1899..39bce9f32c 100644
--- a/esphome/components/ms5611/ms5611.cpp
+++ b/esphome/components/ms5611/ms5611.cpp
@@ -19,7 +19,7 @@ void MS5611Component::setup() {
     this->mark_failed();
     return;
   }
-  delay(100);
+  delay(100);  // NOLINT
   for (uint8_t offset = 0; offset < 6; offset++) {
     if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) {
       this->mark_failed();
diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py
index 694ac028fc..e5106d4bd6 100644
--- a/esphome/components/neopixelbus/light.py
+++ b/esphome/components/neopixelbus/light.py
@@ -101,6 +101,14 @@ ESP8266_METHODS = {
 ESP32_METHODS = {
     'ESP32_I2S_0': 'NeoEsp32I2s0{}Method',
     'ESP32_I2S_1': 'NeoEsp32I2s1{}Method',
+    'ESP32_RMT_0': 'NeoEsp32Rmt0{}Method',
+    'ESP32_RMT_1': 'NeoEsp32Rmt1{}Method',
+    'ESP32_RMT_2': 'NeoEsp32Rmt2{}Method',
+    'ESP32_RMT_3': 'NeoEsp32Rmt3{}Method',
+    'ESP32_RMT_4': 'NeoEsp32Rmt4{}Method',
+    'ESP32_RMT_5': 'NeoEsp32Rmt5{}Method',
+    'ESP32_RMT_6': 'NeoEsp32Rmt6{}Method',
+    'ESP32_RMT_7': 'NeoEsp32Rmt7{}Method',
     'BIT_BANG': 'NeoEsp32BitBang{}Method',
 }
 
@@ -160,4 +168,5 @@ def to_code(config):
 
     cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE])))
 
-    cg.add_library('NeoPixelBus', '2.4.1')
+    # https://github.com/Makuna/NeoPixelBus/blob/master/library.json
+    cg.add_library('NeoPixelBus-esphome', '2.5.2')
diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index f41a97ce7e..e594e147f4 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -46,7 +46,7 @@ void Nextion::set_component_value(const char *component, int value) {
   this->send_command_printf("%s.val=%d", component, value);
 }
 void Nextion::display_picture(int picture_id, int x_start, int y_start) {
-  this->send_command_printf("pic %d %d %d", picture_id, x_start, y_start);
+  this->send_command_printf("pic %d %d %d", x_start, y_start, picture_id);
 }
 void Nextion::set_component_background_color(const char *component, const char *color) {
   this->send_command_printf("%s.bco=\"%s\"", component, color);
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index 92b41a88af..bd37e241e9 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -394,7 +394,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice {
   bool wait_for_ack_{true};
 };
 
-class NextionTouchComponent : public binary_sensor::BinarySensor {
+class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff {
  public:
   void set_page_id(uint8_t page_id) { page_id_ = page_id; }
   void set_component_id(uint8_t component_id) { component_id_ = component_id; }
diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp
index 1b5c5182c7..9446508b0b 100644
--- a/esphome/components/ntc/ntc.cpp
+++ b/esphome/components/ntc/ntc.cpp
@@ -19,9 +19,9 @@ void NTC::process_(float value) {
     return;
   }
 
-  float lr = logf(value);
-  float v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
-  float temp = 1 / v - 273.15f;
+  double lr = log(double(value));
+  double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
+  auto temp = float(1.0 / v - 273.15);
 
   ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
   this->publish_state(temp);
diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h
index 9d6b37412d..c8592e0fe8 100644
--- a/esphome/components/ntc/ntc.h
+++ b/esphome/components/ntc/ntc.h
@@ -9,9 +9,9 @@ namespace ntc {
 class NTC : public Component, public sensor::Sensor {
  public:
   void set_sensor(Sensor *sensor) { sensor_ = sensor; }
-  void set_a(float a) { a_ = a; }
-  void set_b(float b) { b_ = b; }
-  void set_c(float c) { c_ = c; }
+  void set_a(double a) { a_ = a; }
+  void set_b(double b) { b_ = b; }
+  void set_c(double c) { c_ = c; }
   void setup() override;
   void dump_config() override;
   float get_setup_priority() const override;
@@ -20,9 +20,9 @@ class NTC : public Component, public sensor::Sensor {
   void process_(float value);
 
   sensor::Sensor *sensor_;
-  float a_;
-  float b_;
-  float c_;
+  double a_;
+  double b_;
+  double c_;
 };
 
 }  // namespace ntc
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index e290e57baf..869de777d6 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -22,6 +22,8 @@ def to_code(config):
     cg.add(var.set_port(config[CONF_PORT]))
     cg.add(var.set_auth_password(config[CONF_PASSWORD]))
 
+    yield cg.register_component(var, config)
+
     if config[CONF_SAFE_MODE]:
         cg.add(var.start_safe_mode())
 
@@ -29,6 +31,3 @@ def to_code(config):
         cg.add_library('Update', None)
     elif CORE.is_esp32:
         cg.add_library('Hash', None)
-
-    # Register at end for safe mode
-    yield cg.register_component(var, config)
diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp
index d37a7a0c6a..2041c688eb 100644
--- a/esphome/components/ota/ota_component.cpp
+++ b/esphome/components/ota/ota_component.cpp
@@ -182,11 +182,11 @@ void OTAComponent::handle_() {
       error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;
       goto error;
     }
-    if (ss.indexOf("new Flash config wrong") != -1) {
+    if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) {
       error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
       goto error;
     }
-    if (ss.indexOf("Flash config wrong real") != -1) {
+    if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) {
       error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
       goto error;
     }
@@ -266,7 +266,7 @@ void OTAComponent::handle_() {
   delay(10);
   ESP_LOGI(TAG, "OTA update finished!");
   this->status_clear_warning();
-  delay(100);
+  delay(100);  // NOLINT
   App.safe_reboot();
 
 error:
@@ -358,7 +358,7 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
   this->safe_mode_start_time_ = millis();
   this->safe_mode_enable_time_ = enable_time;
   this->safe_mode_num_attempts_ = num_attempts;
-  this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL);
+  this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL, false);
   this->safe_mode_rtc_value_ = this->read_rtc_();
 
   ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_);
@@ -369,19 +369,18 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
     ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode.");
 
     this->status_set_error();
-    network_setup();
-    this->call_setup();
+    this->set_timeout(enable_time, []() {
+      ESP_LOGE(TAG, "No OTA attempt made, restarting.");
+      App.reboot();
+    });
+
+    App.setup();
 
     ESP_LOGI(TAG, "Waiting for OTA attempt.");
-    uint32_t begin = millis();
-    while ((millis() - begin) < enable_time) {
-      this->call_loop();
-      network_tick();
-      App.feed_wdt();
-      yield();
+
+    while (true) {
+      App.loop();
     }
-    ESP_LOGE(TAG, "No OTA attempt made, restarting.");
-    App.reboot();
   } else {
     // increment counter
     this->write_rtc_(this->safe_mode_rtc_value_ + 1);
diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp
index d469cf835f..50922e2f48 100644
--- a/esphome/components/pcf8574/pcf8574.cpp
+++ b/esphome/components/pcf8574/pcf8574.cpp
@@ -55,8 +55,6 @@ void PCF8574Component::pin_mode(uint8_t pin, uint8_t mode) {
     default:
       break;
   }
-
-  this->write_gpio_();
 }
 bool PCF8574Component::read_gpio_() {
   if (this->is_failed())
diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp
index 548099a495..489442c637 100644
--- a/esphome/components/pmsx003/pmsx003.cpp
+++ b/esphome/components/pmsx003/pmsx003.cpp
@@ -169,6 +169,7 @@ void PMSX003Component::dump_config() {
   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
   LOG_SENSOR("  ", "Formaldehyde", this->formaldehyde_sensor_);
+  this->check_uart_settings(9600);
 }
 
 }  // namespace pmsx003
diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py
index fa9a92d430..0cbaf1bf29 100644
--- a/esphome/components/pmsx003/sensor.py
+++ b/esphome/components/pmsx003/sensor.py
@@ -12,24 +12,24 @@ pmsx003_ns = cg.esphome_ns.namespace('pmsx003')
 PMSX003Component = pmsx003_ns.class_('PMSX003Component', uart.UARTDevice, cg.Component)
 PMSX003Sensor = pmsx003_ns.class_('PMSX003Sensor', sensor.Sensor)
 
-CONF_PMSX003 = 'PMSX003'
-CONF_PMS5003T = 'PMS5003T'
-CONF_PMS5003ST = 'PMS5003ST'
+TYPE_PMSX003 = 'PMSX003'
+TYPE_PMS5003T = 'PMS5003T'
+TYPE_PMS5003ST = 'PMS5003ST'
 
 PMSX003Type = pmsx003_ns.enum('PMSX003Type')
 PMSX003_TYPES = {
-    CONF_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
-    CONF_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
-    CONF_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST,
+    TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
+    TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
+    TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST,
 }
 
 SENSORS_TO_TYPE = {
-    CONF_PM_1_0: [CONF_PMSX003, CONF_PMS5003ST],
-    CONF_PM_2_5: [CONF_PMSX003, CONF_PMS5003T, CONF_PMS5003ST],
-    CONF_PM_10_0: [CONF_PMSX003, CONF_PMS5003ST],
-    CONF_TEMPERATURE: [CONF_PMS5003T, CONF_PMS5003ST],
-    CONF_HUMIDITY: [CONF_PMS5003T, CONF_PMS5003ST],
-    CONF_FORMALDEHYDE: [CONF_PMS5003ST],
+    CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003ST],
+    CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST],
+    CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003ST],
+    CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST],
+    CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST],
+    CONF_FORMALDEHYDE: [TYPE_PMS5003ST],
 }
 
 
diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp
index 07a41444ce..93000a7421 100644
--- a/esphome/components/pn532/pn532.cpp
+++ b/esphome/components/pn532/pn532.cpp
@@ -335,7 +335,6 @@ bool PN532::wait_ready_() {
   return true;
 }
 
-bool PN532::is_device_msb_first() { return false; }
 void PN532::dump_config() {
   ESP_LOGCONFIG(TAG, "PN532:");
   switch (this->error_code_) {
diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h
index d349c7a150..49d5878265 100644
--- a/esphome/components/pn532/pn532.h
+++ b/esphome/components/pn532/pn532.h
@@ -11,7 +11,9 @@ namespace pn532 {
 class PN532BinarySensor;
 class PN532Trigger;
 
-class PN532 : public PollingComponent, public spi::SPIDevice {
+class PN532 : public PollingComponent,
+              public spi::SPIDevice<spi::BIT_ORDER_LSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
+                                    spi::DATA_RATE_1MHZ> {
  public:
   void setup() override;
 
@@ -26,8 +28,6 @@ class PN532 : public PollingComponent, public spi::SPIDevice {
   void register_trigger(PN532Trigger *trig) { this->triggers_.push_back(trig); }
 
  protected:
-  bool is_device_msb_first() override;
-
   /// Write the full command given in data to the PN532
   void pn532_write_command_(const std::vector<uint8_t> &data);
   bool pn532_write_command_check_ack_(const std::vector<uint8_t> &data);
diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp
index 6503711e35..c71e51eb32 100644
--- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp
+++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp
@@ -149,6 +149,7 @@ void PulseCounterSensor::dump_config() {
   ESP_LOGCONFIG(TAG, "  Rising Edge: %s", EDGE_MODE_TO_STRING[this->storage_.rising_edge_mode]);
   ESP_LOGCONFIG(TAG, "  Falling Edge: %s", EDGE_MODE_TO_STRING[this->storage_.falling_edge_mode]);
   ESP_LOGCONFIG(TAG, "  Filtering pulses shorter than %u µs", this->storage_.filter_us);
+  LOG_UPDATE_INTERVAL(this);
 }
 
 void PulseCounterSensor::update() {
diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py
index 636147c138..e73bc36036 100644
--- a/esphome/components/pulse_counter/sensor.py
+++ b/esphome/components/pulse_counter/sensor.py
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import sensor
 from esphome.const import CONF_COUNT_MODE, CONF_FALLING_EDGE, CONF_ID, CONF_INTERNAL_FILTER, \
-    CONF_PIN, CONF_RISING_EDGE, CONF_UPDATE_INTERVAL, CONF_NUMBER, \
+    CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, \
     ICON_PULSE, UNIT_PULSES_PER_MINUTE
 from esphome.core import CORE
 
@@ -49,7 +49,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).exte
         cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA,
     }),
     cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter,
-    cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval,
 }).extend(cv.polling_component_schema('60s'))
 
 
diff --git a/esphome/components/pzem004t/__init__.py b/esphome/components/pzem004t/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp
new file mode 100644
index 0000000000..cbdc14f0d0
--- /dev/null
+++ b/esphome/components/pzem004t/pzem004t.cpp
@@ -0,0 +1,109 @@
+#include "pzem004t.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace pzem004t {
+
+static const char *TAG = "pzem004t";
+
+void PZEM004T::loop() {
+  const uint32_t now = millis();
+  if (now - this->last_read_ > 500 && this->available()) {
+    while (this->available())
+      this->read();
+    this->last_read_ = now;
+  }
+
+  // PZEM004T packet size is 7 byte
+  while (this->available() >= 7) {
+    auto resp = *this->read_array<7>();
+    // packet format:
+    // 0: packet type
+    // 1-5: data
+    // 6: checksum (sum of other bytes)
+    // see https://github.com/olehs/PZEM004T
+    uint8_t sum = 0;
+    for (int i = 0; i < 6; i++)
+      sum += resp[i];
+
+    if (sum != resp[6]) {
+      ESP_LOGV(TAG, "PZEM004T invalid checksum! 0x%02X != 0x%02X", sum, resp[6]);
+      continue;
+    }
+
+    switch (resp[0]) {
+      case 0xA4: {  // Set Module Address Response
+        this->write_state_(READ_VOLTAGE);
+        break;
+      }
+      case 0xA0: {  // Voltage Response
+        uint16_t int_voltage = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0);
+        float voltage = int_voltage + (resp[3] / 10.0f);
+        if (this->voltage_sensor_ != nullptr)
+          this->voltage_sensor_->publish_state(voltage);
+        ESP_LOGD(TAG, "Got Voltage %.1f V", voltage);
+        this->write_state_(READ_CURRENT);
+        break;
+      }
+      case 0xA1: {  // Current Response
+        uint16_t int_current = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0);
+        float current = int_current + (resp[3] / 100.0f);
+        if (this->current_sensor_ != nullptr)
+          this->current_sensor_->publish_state(current);
+        ESP_LOGD(TAG, "Got Current %.2f A", current);
+        this->write_state_(READ_POWER);
+        break;
+      }
+      case 0xA2: {  // Active Power Response
+        uint16_t power = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0);
+        if (this->power_sensor_ != nullptr)
+          this->power_sensor_->publish_state(power);
+        ESP_LOGD(TAG, "Got Power %u W", power);
+        this->write_state_(DONE);
+        break;
+      }
+
+      case 0xA3:  // Energy Response
+      case 0xA5:  // Set Power Alarm Response
+      case 0xB0:  // Voltage Request
+      case 0xB1:  // Current Request
+      case 0xB2:  // Active Power Response
+      case 0xB3:  // Energy Request
+      case 0xB4:  // Set Module Address Request
+      case 0xB5:  // Set Power Alarm Request
+      default:
+        break;
+    }
+
+    this->last_read_ = now;
+  }
+}
+void PZEM004T::update() { this->write_state_(SET_ADDRESS); }
+void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) {
+  if (state == DONE) {
+    this->read_state_ = state;
+    return;
+  }
+  std::array<uint8_t, 7> data{};
+  data[0] = state;
+  data[1] = 192;
+  data[2] = 168;
+  data[3] = 1;
+  data[4] = 1;
+  data[5] = 0;
+  data[6] = 0;
+  for (int i = 0; i < 6; i++)
+    data[6] += data[i];
+
+  this->write_array(data);
+  this->read_state_ = state;
+}
+void PZEM004T::dump_config() {
+  ESP_LOGCONFIG(TAG, "PZEM004T:");
+  LOG_SENSOR("", "Voltage", this->voltage_sensor_);
+  LOG_SENSOR("", "Current", this->current_sensor_);
+  LOG_SENSOR("", "Power", this->power_sensor_);
+}
+
+}  // namespace pzem004t
+}  // namespace esphome
diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h
new file mode 100644
index 0000000000..f0208d415a
--- /dev/null
+++ b/esphome/components/pzem004t/pzem004t.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/uart/uart.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace pzem004t {
+
+class PZEM004T : public PollingComponent, public uart::UARTDevice {
+ public:
+  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
+  void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
+  void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
+
+  void loop() override;
+
+  void update() override;
+
+  void dump_config() override;
+
+ protected:
+  sensor::Sensor *voltage_sensor_;
+  sensor::Sensor *current_sensor_;
+  sensor::Sensor *power_sensor_;
+
+  enum PZEM004TReadState {
+    SET_ADDRESS = 0xB4,
+    READ_VOLTAGE = 0xB0,
+    READ_CURRENT = 0xB1,
+    READ_POWER = 0xB2,
+    DONE = 0x00,
+  } read_state_{DONE};
+
+  void write_state_(PZEM004TReadState state);
+
+  uint32_t last_read_{0};
+};
+
+}  // namespace pzem004t
+}  // namespace esphome
diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py
new file mode 100644
index 0000000000..6e3628c5ec
--- /dev/null
+++ b/esphome/components/pzem004t/sensor.py
@@ -0,0 +1,37 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, uart
+from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \
+    UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT
+
+DEPENDENCIES = ['uart']
+
+pzem004t_ns = cg.esphome_ns.namespace('pzem004t')
+PZEM004T = pzem004t_ns.class_('PZEM004T', cg.PollingComponent, uart.UARTDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(PZEM004T),
+
+    cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1),
+    cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2),
+    cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0),
+}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield uart.register_uart_device(var, config)
+
+    if CONF_VOLTAGE in config:
+        conf = config[CONF_VOLTAGE]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_voltage_sensor(sens))
+    if CONF_CURRENT in config:
+        conf = config[CONF_CURRENT]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_current_sensor(sens))
+    if CONF_POWER in config:
+        conf = config[CONF_POWER]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_power_sensor(sens))
diff --git a/esphome/components/pzemac/__init__.py b/esphome/components/pzemac/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp
new file mode 100644
index 0000000000..f05ce15711
--- /dev/null
+++ b/esphome/components/pzemac/pzemac.cpp
@@ -0,0 +1,71 @@
+#include "pzemac.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace pzemac {
+
+static const char *TAG = "pzemac";
+
+static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04;
+static const uint8_t PZEM_REGISTER_COUNT = 10;  // 10x 16-bit registers
+
+void PZEMAC::on_modbus_data(const std::vector<uint8_t> &data) {
+  if (data.size() < 20) {
+    ESP_LOGW(TAG, "Invalid size for PZEM AC!");
+    return;
+  }
+
+  // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
+  //  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
+  // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34
+  // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc--
+
+  auto pzem_get_16bit = [&](size_t i) -> uint16_t {
+    return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
+  };
+  auto pzem_get_32bit = [&](size_t i) -> uint32_t {
+    return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0);
+  };
+
+  uint16_t raw_voltage = pzem_get_16bit(0);
+  float voltage = raw_voltage / 10.0f;  // max 6553.5 V
+
+  uint32_t raw_current = pzem_get_32bit(2);
+  float current = raw_current / 1000.0f;  // max 4294967.295 A
+
+  uint32_t raw_active_power = pzem_get_32bit(6);
+  float active_power = raw_active_power / 10.0f;  // max 429496729.5 W
+
+  uint16_t raw_frequency = pzem_get_16bit(14);
+  float frequency = raw_frequency / 10.0f;
+
+  uint16_t raw_power_factor = pzem_get_16bit(16);
+  float power_factor = raw_power_factor / 100.0f;
+
+  ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency,
+           power_factor);
+  if (this->voltage_sensor_ != nullptr)
+    this->voltage_sensor_->publish_state(voltage);
+  if (this->current_sensor_ != nullptr)
+    this->current_sensor_->publish_state(current);
+  if (this->power_sensor_ != nullptr)
+    this->power_sensor_->publish_state(active_power);
+  if (this->frequency_sensor_ != nullptr)
+    this->frequency_sensor_->publish_state(frequency);
+  if (this->power_factor_sensor_ != nullptr)
+    this->power_factor_sensor_->publish_state(power_factor);
+}
+
+void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); }
+void PZEMAC::dump_config() {
+  ESP_LOGCONFIG(TAG, "PZEMAC:");
+  ESP_LOGCONFIG(TAG, "  Address: 0x%02X", this->address_);
+  LOG_SENSOR("", "Voltage", this->voltage_sensor_);
+  LOG_SENSOR("", "Current", this->current_sensor_);
+  LOG_SENSOR("", "Power", this->power_sensor_);
+  LOG_SENSOR("", "Frequency", this->frequency_sensor_);
+  LOG_SENSOR("", "Power Factor", this->power_factor_sensor_);
+}
+
+}  // namespace pzemac
+}  // namespace esphome
diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h
new file mode 100644
index 0000000000..d396b7cddf
--- /dev/null
+++ b/esphome/components/pzemac/pzemac.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/modbus/modbus.h"
+
+namespace esphome {
+namespace pzemac {
+
+class PZEMAC : public PollingComponent, public modbus::ModbusDevice {
+ public:
+  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
+  void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
+  void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
+  void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
+  void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; }
+
+  void update() override;
+
+  void on_modbus_data(const std::vector<uint8_t> &data) override;
+
+  void dump_config() override;
+
+ protected:
+  sensor::Sensor *voltage_sensor_;
+  sensor::Sensor *current_sensor_;
+  sensor::Sensor *power_sensor_;
+  sensor::Sensor *frequency_sensor_;
+  sensor::Sensor *power_factor_sensor_;
+};
+
+}  // namespace pzemac
+}  // namespace esphome
diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py
new file mode 100644
index 0000000000..35d8069767
--- /dev/null
+++ b/esphome/components/pzemac/sensor.py
@@ -0,0 +1,47 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, modbus
+from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \
+    CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \
+    ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC
+
+AUTO_LOAD = ['modbus']
+
+pzemac_ns = cg.esphome_ns.namespace('pzemac')
+PZEMAC = pzemac_ns.class_('PZEMAC', cg.PollingComponent, modbus.ModbusDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(PZEMAC),
+    cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1),
+    cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3),
+    cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1),
+    cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_EMPTY, ICON_CURRENT_AC, 1),
+    cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2),
+}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield modbus.register_modbus_device(var, config)
+
+    if CONF_VOLTAGE in config:
+        conf = config[CONF_VOLTAGE]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_voltage_sensor(sens))
+    if CONF_CURRENT in config:
+        conf = config[CONF_CURRENT]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_current_sensor(sens))
+    if CONF_POWER in config:
+        conf = config[CONF_POWER]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_power_sensor(sens))
+    if CONF_FREQUENCY in config:
+        conf = config[CONF_FREQUENCY]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_frequency_sensor(sens))
+    if CONF_POWER_FACTOR in config:
+        conf = config[CONF_POWER_FACTOR]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_power_factor_sensor(sens))
diff --git a/esphome/components/pzemdc/__init__.py b/esphome/components/pzemdc/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp
new file mode 100644
index 0000000000..9bd58410c0
--- /dev/null
+++ b/esphome/components/pzemdc/pzemdc.cpp
@@ -0,0 +1,59 @@
+#include "pzemdc.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace pzemdc {
+
+static const char *TAG = "pzemdc";
+
+static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04;
+static const uint8_t PZEM_REGISTER_COUNT = 10;  // 10x 16-bit registers
+
+void PZEMDC::on_modbus_data(const std::vector<uint8_t> &data) {
+  if (data.size() < 16) {
+    ESP_LOGW(TAG, "Invalid size for PZEM DC!");
+    return;
+  }
+
+  // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
+  //           0     1     2     3     4     5     6     7           = ModBus register
+  //  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20  = Buffer index
+  // 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29
+  // Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc--
+
+  auto pzem_get_16bit = [&](size_t i) -> uint16_t {
+    return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
+  };
+  auto pzem_get_32bit = [&](size_t i) -> uint32_t {
+    return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0);
+  };
+
+  uint16_t raw_voltage = pzem_get_16bit(0);
+  float voltage = raw_voltage / 100.0f;  // max 655.35 V
+
+  uint16_t raw_current = pzem_get_16bit(2);
+  float current = raw_current / 100.0f;  // max 655.35 A
+
+  uint32_t raw_power = pzem_get_32bit(4);
+  float power = raw_power / 10.0f;  // max 429496729.5 W
+
+  ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power);
+  if (this->voltage_sensor_ != nullptr)
+    this->voltage_sensor_->publish_state(voltage);
+  if (this->current_sensor_ != nullptr)
+    this->current_sensor_->publish_state(current);
+  if (this->power_sensor_ != nullptr)
+    this->power_sensor_->publish_state(power);
+}
+
+void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); }
+void PZEMDC::dump_config() {
+  ESP_LOGCONFIG(TAG, "PZEMDC:");
+  ESP_LOGCONFIG(TAG, "  Address: 0x%02X", this->address_);
+  LOG_SENSOR("", "Voltage", this->voltage_sensor_);
+  LOG_SENSOR("", "Current", this->current_sensor_);
+  LOG_SENSOR("", "Power", this->power_sensor_);
+}
+
+}  // namespace pzemdc
+}  // namespace esphome
diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h
new file mode 100644
index 0000000000..d838eb4167
--- /dev/null
+++ b/esphome/components/pzemdc/pzemdc.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/modbus/modbus.h"
+
+namespace esphome {
+namespace pzemdc {
+
+class PZEMDC : public PollingComponent, public modbus::ModbusDevice {
+ public:
+  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
+  void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
+  void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
+  void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
+  void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; }
+
+  void update() override;
+
+  void on_modbus_data(const std::vector<uint8_t> &data) override;
+
+  void dump_config() override;
+
+ protected:
+  sensor::Sensor *voltage_sensor_;
+  sensor::Sensor *current_sensor_;
+  sensor::Sensor *power_sensor_;
+  sensor::Sensor *frequency_sensor_;
+  sensor::Sensor *power_factor_sensor_;
+};
+
+}  // namespace pzemdc
+}  // namespace esphome
diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py
new file mode 100644
index 0000000000..8c6fd08868
--- /dev/null
+++ b/esphome/components/pzemdc/sensor.py
@@ -0,0 +1,36 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, modbus
+from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \
+    UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, ICON_POWER, ICON_CURRENT_AC
+
+AUTO_LOAD = ['modbus']
+
+pzemdc_ns = cg.esphome_ns.namespace('pzemdc')
+PZEMDC = pzemdc_ns.class_('PZEMDC', cg.PollingComponent, modbus.ModbusDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(PZEMDC),
+    cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1),
+    cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3),
+    cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1),
+}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield modbus.register_modbus_device(var, config)
+
+    if CONF_VOLTAGE in config:
+        conf = config[CONF_VOLTAGE]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_voltage_sensor(sens))
+    if CONF_CURRENT in config:
+        conf = config[CONF_CURRENT]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_current_sensor(sens))
+    if CONF_POWER in config:
+        conf = config[CONF_POWER]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_power_sensor(sens))
diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h
index a67b6e7ce8..13df400754 100644
--- a/esphome/components/rdm6300/rdm6300.h
+++ b/esphome/components/rdm6300/rdm6300.h
@@ -28,7 +28,7 @@ class RDM6300Component : public Component, public uart::UARTDevice {
   uint32_t last_id_{0};
 };
 
-class RDM6300BinarySensor : public binary_sensor::BinarySensor {
+class RDM6300BinarySensor : public binary_sensor::BinarySensorInitiallyOff {
  public:
   void set_id(uint32_t id) { id_ = id; }
 
diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py
index d3da238b08..a62304c87d 100644
--- a/esphome/components/remote_base/__init__.py
+++ b/esphome/components/remote_base/__init__.py
@@ -88,7 +88,7 @@ def validate_repeat(value):
         return cv.Schema({
             cv.Required(CONF_TIMES): cv.templatable(cv.positive_int),
             cv.Optional(CONF_WAIT_TIME, default='10ms'):
-                cv.templatable(cv.positive_time_period_milliseconds),
+                cv.templatable(cv.positive_time_period_microseconds),
         })(value)
     return validate_repeat({CONF_TIMES: value})
 
@@ -250,7 +250,7 @@ def lg_dumper(var, config):
 def lg_action(var, config, args):
     template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32)
     cg.add(var.set_data(template_))
-    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint8)
+    template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint8)
     cg.add(var.set_nbits(template_))
 
 
@@ -420,7 +420,7 @@ def rc5_action(var, config, args):
 RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2))
 
 RC_SWITCH_PROTOCOL_SCHEMA = cv.Any(
-    cv.int_range(min=1, max=7),
+    cv.int_range(min=1, max=8),
     cv.Schema({
         cv.Required(CONF_PULSE_LENGTH): cv.uint32_t,
         cv.Optional(CONF_SYNC, default=[1, 31]): RC_SWITCH_TIMING_SCHEMA,
@@ -438,14 +438,30 @@ def validate_rc_switch_code(value):
         if c not in ('0', '1'):
             raise cv.Invalid(u"Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed"
                              u"".format(c))
-    if len(value) > 32:
-        raise cv.Invalid("Maximum length for RCSwitch codes is 32, code '{}' has length {}"
+    if len(value) > 64:
+        raise cv.Invalid("Maximum length for RCSwitch codes is 64, code '{}' has length {}"
                          "".format(value, len(value)))
     if not value:
         raise cv.Invalid("RCSwitch code must not be empty")
     return value
 
 
+def validate_rc_switch_raw_code(value):
+    if not isinstance(value, (str, text_type)):
+        raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')")
+    for c in value:
+        if c not in ('0', '1', 'x'):
+            raise cv.Invalid(
+                "Invalid RCSwitch raw code character '{}'.Only '0', '1' and 'x' are allowed"
+                .format(c))
+    if len(value) > 64:
+        raise cv.Invalid("Maximum length for RCSwitch raw codes is 64, code '{}' has length {}"
+                         "".format(value, len(value)))
+    if not value:
+        raise cv.Invalid("RCSwitch raw code must not be empty")
+    return value
+
+
 def build_rc_switch_protocol(config):
     if isinstance(config, int):
         return rc_switch_protocols[config]
@@ -457,7 +473,7 @@ def build_rc_switch_protocol(config):
 
 
 RC_SWITCH_RAW_SCHEMA = cv.Schema({
-    cv.Required(CONF_CODE): validate_rc_switch_code,
+    cv.Required(CONF_CODE): validate_rc_switch_raw_code,
     cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA,
 })
 RC_SWITCH_TYPE_A_SCHEMA = cv.Schema({
@@ -490,7 +506,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema({
     cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema({
         cv.Required(CONF_TIMES): cv.templatable(cv.positive_int),
         cv.Optional(CONF_WAIT_TIME, default='10ms'):
-            cv.templatable(cv.positive_time_period_milliseconds),
+            cv.templatable(cv.positive_time_period_microseconds),
     }),
 })
 
@@ -624,7 +640,7 @@ def samsung_dumper(var, config):
 
 @register_action('samsung', SamsungAction, SAMSUNG_SCHEMA)
 def samsung_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint16)
+    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32)
     cg.add(var.set_data(template_))
 
 
diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp
index 029f1fccf2..754b2fae49 100644
--- a/esphome/components/remote_base/rc_switch_protocol.cpp
+++ b/esphome/components/remote_base/rc_switch_protocol.cpp
@@ -6,14 +6,15 @@ namespace remote_base {
 
 static const char *TAG = "remote.rc_switch";
 
-RCSwitchBase rc_switch_protocols[8] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false),
+RCSwitchBase rc_switch_protocols[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false),
                                        RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false),
                                        RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false),
                                        RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false),
                                        RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false),
                                        RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false),
                                        RCSwitchBase(10350, 450, 450, 900, 900, 450, true),
-                                       RCSwitchBase(300, 9300, 150, 900, 900, 150, false)};
+                                       RCSwitchBase(300, 9300, 150, 900, 900, 150, false),
+                                       RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)};
 
 RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low,
                            uint32_t one_high, uint32_t one_low, bool inverted)
@@ -52,7 +53,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const {
     dst->mark(this->sync_low_);
   }
 }
-void RCSwitchBase::transmit(RemoteTransmitData *dst, uint32_t code, uint8_t len) const {
+void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const {
   dst->set_carrier_frequency(0);
   for (int16_t i = len - 1; i >= 0; i--) {
     if (code & (1 << i))
@@ -108,12 +109,12 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const {
   src.advance(2);
   return true;
 }
-bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *out_nbits) const {
+bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const {
   // ignore if sync doesn't exist
   this->expect_sync(src);
 
   *out_data = 0;
-  for (*out_nbits = 1; *out_nbits < 32; *out_nbits += 1) {
+  for (*out_nbits = 0; *out_nbits < 64; *out_nbits += 1) {
     if (this->expect_zero(src)) {
       *out_data <<= 1;
       *out_data |= 0;
@@ -121,14 +122,13 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *o
       *out_data <<= 1;
       *out_data |= 1;
     } else {
-      *out_nbits -= 1;
       return *out_nbits >= 8;
     }
   }
   return true;
 }
 
-void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_t *out_code) {
+void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code) {
   *out_code = 0;
   for (int8_t i = nbits - 1; i >= 0; i--) {
     *out_code <<= 2;
@@ -138,24 +138,18 @@ void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_
       *out_code |= 0b00;
   }
 }
-void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint32_t *out_code,
+void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code,
                                uint8_t *out_nbits) {
   uint16_t code = 0;
-  code |= (switch_group & 0b0001) ? 0 : 0b1000;
-  code |= (switch_group & 0b0010) ? 0 : 0b0100;
-  code |= (switch_group & 0b0100) ? 0 : 0b0010;
-  code |= (switch_group & 0b1000) ? 0 : 0b0001;
-  code <<= 4;
-  code |= (switch_device & 0b0001) ? 0 : 0b1000;
-  code |= (switch_device & 0b0010) ? 0 : 0b0100;
-  code |= (switch_device & 0b0100) ? 0 : 0b0010;
-  code |= (switch_device & 0b1000) ? 0 : 0b0001;
+  code = switch_group ^ 0b11111;
+  code <<= 5;
+  code |= switch_device ^ 0b11111;
   code <<= 2;
   code |= state ? 0b01 : 0b10;
-  simple_code_to_tristate(code, 10, out_code);
-  *out_nbits = 20;
+  simple_code_to_tristate(code, 12, out_code);
+  *out_nbits = 24;
 }
-void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint32_t *out_code,
+void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint64_t *out_code,
                                uint8_t *out_nbits) {
   uint16_t code = 0;
   code |= (address_code == 1) ? 0 : 0b1000;
@@ -173,7 +167,7 @@ void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool
   simple_code_to_tristate(code, 12, out_code);
   *out_nbits = 24;
 }
-void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint32_t *out_code,
+void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint64_t *out_code,
                                uint8_t *out_nbits) {
   uint16_t code = 0;
   code |= (family & 0b0001) ? 0b1000 : 0;
@@ -191,7 +185,7 @@ void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bo
   simple_code_to_tristate(code, 12, out_code);
   *out_nbits = 24;
 }
-void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint32_t *out_code, uint8_t *out_nbits) {
+void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits) {
   *out_code = 0;
   *out_code |= (group == 0) ? 0b11000000 : 0b01000000;
   *out_code |= (group == 1) ? 0b00110000 : 0b00010000;
@@ -208,8 +202,8 @@ void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint32
   *out_nbits = 24;
 }
 
-uint32_t decode_binary_string(const std::string &data) {
-  uint32_t ret = 0;
+uint64_t decode_binary_string(const std::string &data) {
+  uint64_t ret = 0;
   for (char c : data) {
     ret <<= 1UL;
     ret |= (c != '0');
@@ -217,22 +211,31 @@ uint32_t decode_binary_string(const std::string &data) {
   return ret;
 }
 
+uint64_t decode_binary_string_mask(const std::string &data) {
+  uint64_t ret = 0;
+  for (char c : data) {
+    ret <<= 1UL;
+    ret |= (c != 'x');
+  }
+  return ret;
+}
+
 bool RCSwitchRawReceiver::matches(RemoteReceiveData src) {
-  uint32_t decoded_code;
+  uint64_t decoded_code;
   uint8_t decoded_nbits;
   if (!this->protocol_.decode(src, &decoded_code, &decoded_nbits))
     return false;
 
-  return decoded_nbits == this->nbits_ && decoded_code == this->code_;
+  return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_);
 }
 bool RCSwitchDumper::dump(RemoteReceiveData src) {
-  for (uint8_t i = 1; i <= 7; i++) {
+  for (uint8_t i = 1; i <= 8; i++) {
     src.reset();
-    uint32_t out_data;
+    uint64_t out_data;
     uint8_t out_nbits;
     RCSwitchBase *protocol = &rc_switch_protocols[i];
     if (protocol->decode(src, &out_data, &out_nbits) && out_nbits >= 3) {
-      char buffer[33];
+      char buffer[65];
       for (uint8_t j = 0; j < out_nbits; j++)
         buffer[j] = (out_data & (1 << (out_nbits - j - 1))) ? '1' : '0';
 
diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h
index 728561c140..0983da27ea 100644
--- a/esphome/components/remote_base/rc_switch_protocol.h
+++ b/esphome/components/remote_base/rc_switch_protocol.h
@@ -18,7 +18,7 @@ class RCSwitchBase {
 
   void sync(RemoteTransmitData *dst) const;
 
-  void transmit(RemoteTransmitData *dst, uint32_t code, uint8_t len) const;
+  void transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const;
 
   bool expect_one(RemoteReceiveData &src) const;
 
@@ -26,20 +26,20 @@ class RCSwitchBase {
 
   bool expect_sync(RemoteReceiveData &src) const;
 
-  bool decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *out_nbits) const;
+  bool decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const;
 
-  static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_t *out_code);
+  static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code);
 
-  static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint32_t *out_code,
+  static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code,
                           uint8_t *out_nbits);
 
-  static void type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint32_t *out_code,
+  static void type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint64_t *out_code,
                           uint8_t *out_nbits);
 
-  static void type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint32_t *out_code,
+  static void type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint64_t *out_code,
                           uint8_t *out_nbits);
 
-  static void type_d_code(uint8_t group, uint8_t device, bool state, uint32_t *out_code, uint8_t *out_nbits);
+  static void type_d_code(uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits);
 
  protected:
   uint32_t sync_high_{};
@@ -51,9 +51,11 @@ class RCSwitchBase {
   bool inverted_{};
 };
 
-extern RCSwitchBase rc_switch_protocols[8];
+extern RCSwitchBase rc_switch_protocols[9];
 
-uint32_t decode_binary_string(const std::string &data);
+uint64_t decode_binary_string(const std::string &data);
+
+uint64_t decode_binary_string_mask(const std::string &data);
 
 template<typename... Ts> class RCSwitchRawAction : public RemoteTransmitterActionBase<Ts...> {
  public:
@@ -62,7 +64,7 @@ template<typename... Ts> class RCSwitchRawAction : public RemoteTransmitterActio
 
   void encode(RemoteTransmitData *dst, Ts... x) override {
     auto code = this->code_.value(x...);
-    uint32_t the_code = decode_binary_string(code);
+    uint64_t the_code = decode_binary_string(code);
     uint8_t nbits = code.size();
 
     auto proto = this->protocol_.value(x...);
@@ -84,7 +86,7 @@ template<typename... Ts> class RCSwitchTypeAAction : public RemoteTransmitterAct
     uint8_t u_group = decode_binary_string(group);
     uint8_t u_device = decode_binary_string(device);
 
-    uint32_t code;
+    uint64_t code;
     uint8_t nbits;
     RCSwitchBase::type_a_code(u_group, u_device, state, &code, &nbits);
 
@@ -105,7 +107,7 @@ template<typename... Ts> class RCSwitchTypeBAction : public RemoteTransmitterAct
     auto channel = this->channel_.value(x...);
     auto state = this->state_.value(x...);
 
-    uint32_t code;
+    uint64_t code;
     uint8_t nbits;
     RCSwitchBase::type_b_code(address, channel, state, &code, &nbits);
 
@@ -130,7 +132,7 @@ template<typename... Ts> class RCSwitchTypeCAction : public RemoteTransmitterAct
 
     auto u_family = static_cast<uint8_t>(tolower(family[0]) - 'a');
 
-    uint32_t code;
+    uint64_t code;
     uint8_t nbits;
     RCSwitchBase::type_c_code(u_family, group, device, state, &code, &nbits);
 
@@ -152,7 +154,7 @@ template<typename... Ts> class RCSwitchTypeDAction : public RemoteTransmitterAct
 
     auto u_group = static_cast<uint8_t>(tolower(group[0]) - 'a');
 
-    uint32_t code;
+    uint64_t code;
     uint8_t nbits;
     RCSwitchBase::type_d_code(u_group, device, state, &code, &nbits);
 
@@ -164,9 +166,10 @@ template<typename... Ts> class RCSwitchTypeDAction : public RemoteTransmitterAct
 class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase {
  public:
   void set_protocol(const RCSwitchBase &a_protocol) { this->protocol_ = a_protocol; }
-  void set_code(uint32_t code) { this->code_ = code; }
+  void set_code(uint64_t code) { this->code_ = code; }
   void set_code(const std::string &code) {
     this->code_ = decode_binary_string(code);
+    this->mask_ = decode_binary_string_mask(code);
     this->nbits_ = code.size();
   }
   void set_nbits(uint8_t nbits) { this->nbits_ = nbits; }
@@ -191,7 +194,8 @@ class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase {
   bool matches(RemoteReceiveData src) override;
 
   RCSwitchBase protocol_;
-  uint32_t code_;
+  uint64_t code_;
+  uint64_t mask_{0xFFFFFFFFFFFFFFFF};
   uint8_t nbits_;
 };
 
diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h
index 6035e2fd57..36be25add7 100644
--- a/esphome/components/remote_base/remote_base.h
+++ b/esphome/components/remote_base/remote_base.h
@@ -267,11 +267,11 @@ class RemoteReceiverBase : public RemoteComponentBase {
   uint8_t tolerance_{25};
 };
 
-class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensor,
+class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitiallyOff,
                                        public Component,
                                        public RemoteReceiverListener {
  public:
-  explicit RemoteReceiverBinarySensorBase() : BinarySensor() {}
+  explicit RemoteReceiverBinarySensorBase() : BinarySensorInitiallyOff() {}
   void dump_config() override;
   virtual bool matches(RemoteReceiveData src) = 0;
   bool on_receive(RemoteReceiveData src) override {
diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/restart_switch.cpp
index cd0979b9d3..f66ebc616e 100644
--- a/esphome/components/restart/restart_switch.cpp
+++ b/esphome/components/restart/restart_switch.cpp
@@ -13,8 +13,8 @@ void RestartSwitch::write_state(bool state) {
 
   if (state) {
     ESP_LOGI(TAG, "Restarting device...");
-    // then execute
-    delay(100);  // Let MQTT settle a bit
+    // Let MQTT settle a bit
+    delay(100);  // NOLINT
     App.safe_reboot();
   }
 }
diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h
index b627a4e57f..4220645478 100644
--- a/esphome/components/rotary_encoder/rotary_encoder.h
+++ b/esphome/components/rotary_encoder/rotary_encoder.h
@@ -2,6 +2,7 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/esphal.h"
+#include "esphome/core/automation.h"
 #include "esphome/components/sensor/sensor.h"
 
 namespace esphome {
@@ -43,6 +44,12 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component {
    */
   void set_resolution(RotaryEncoderResolution mode);
 
+  /// Manually set the value of the counter.
+  void set_value(int value) {
+    this->store_.counter = value;
+    this->loop();
+  }
+
   void set_reset_pin(GPIOPin *pin_i) { this->pin_i_ = pin_i; }
   void set_min_value(int32_t min_value);
   void set_max_value(int32_t max_value);
@@ -63,5 +70,15 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component {
   RotaryEncoderSensorStore store_{};
 };
 
+template<typename... Ts> class RotaryEncoderSetValueAction : public Action<Ts...> {
+ public:
+  RotaryEncoderSetValueAction(RotaryEncoderSensor *encoder) : encoder_(encoder) {}
+  TEMPLATABLE_VALUE(int, value)
+  void play(Ts... x) override { this->encoder_->set_value(this->value_.value(x...)); }
+
+ protected:
+  RotaryEncoderSensor *encoder_;
+};
+
 }  // namespace rotary_encoder
 }  // namespace esphome
diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py
index fb4881e18b..214ccbd056 100644
--- a/esphome/components/rotary_encoder/sensor.py
+++ b/esphome/components/rotary_encoder/sensor.py
@@ -1,9 +1,9 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome import pins
+from esphome import pins, automation
 from esphome.components import sensor
 from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \
-    ICON_ROTATE_RIGHT
+    ICON_ROTATE_RIGHT, CONF_VALUE, CONF_PIN_A, CONF_PIN_B
 
 rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder')
 RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution')
@@ -13,11 +13,11 @@ RESOLUTIONS = {
     4: RotaryEncoderResolution.ROTARY_ENCODER_4_PULSES_PER_CYCLE,
 }
 
-CONF_PIN_A = 'pin_a'
-CONF_PIN_B = 'pin_b'
 CONF_PIN_RESET = 'pin_reset'
 
 RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component)
+RotaryEncoderSetValueAction = rotary_encoder_ns.class_('RotaryEncoderSetValueAction',
+                                                       automation.Action)
 
 
 def validate_min_max_value(config):
@@ -60,3 +60,16 @@ def to_code(config):
         cg.add(var.set_min_value(config[CONF_MIN_VALUE]))
     if CONF_MAX_VALUE in config:
         cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
+
+
+@automation.register_action('sensor.rotary_encoder.set_value', RotaryEncoderSetValueAction,
+                            cv.Schema({
+                                cv.Required(CONF_ID): cv.use_id(sensor.Sensor),
+                                cv.Required(CONF_VALUE): cv.templatable(cv.int_),
+                            }))
+def sensor_template_publish_to_code(config, action_id, template_arg, args):
+    paren = yield cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+    template_ = yield cg.templatable(config[CONF_VALUE], args, int)
+    cg.add(var.set_value(template_))
+    yield var
diff --git a/esphome/components/scd30/__init__.py b/esphome/components/scd30/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp
new file mode 100644
index 0000000000..55ab07879e
--- /dev/null
+++ b/esphome/components/scd30/scd30.cpp
@@ -0,0 +1,181 @@
+#include "scd30.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace scd30 {
+
+static const char *TAG = "scd30";
+
+static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100;
+static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010;
+static const uint16_t SCD30_CMD_GET_DATA_READY_STATUS = 0x0202;
+static const uint16_t SCD30_CMD_READ_MEASUREMENT = 0x0300;
+
+/// Commands for future use
+static const uint16_t SCD30_CMD_STOP_MEASUREMENTS = 0x0104;
+static const uint16_t SCD30_CMD_MEASUREMENT_INTERVAL = 0x4600;
+static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306;
+static const uint16_t SCD30_CMD_FORCED_CALIBRATION = 0x5204;
+static const uint16_t SCD30_CMD_TEMPERATURE_OFFSET = 0x5403;
+static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102;
+static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304;
+
+void SCD30Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up scd30...");
+
+  /// Firmware version identification
+  if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) {
+    this->error_code_ = COMMUNICATION_FAILED;
+    this->mark_failed();
+    return;
+  }
+  uint16_t raw_firmware_version[3];
+
+  if (!this->read_data_(raw_firmware_version, 3)) {
+    this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED;
+    this->mark_failed();
+    return;
+  }
+  ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8),
+           uint16_t(raw_firmware_version[0] & 0xFF));
+
+  /// Sensor initialization
+  if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS)) {
+    ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements.");
+    this->error_code_ = MEASUREMENT_INIT_FAILED;
+    this->mark_failed();
+    return;
+  }
+}
+
+void SCD30Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "scd30:");
+  LOG_I2C_DEVICE(this);
+  if (this->is_failed()) {
+    switch (this->error_code_) {
+      case COMMUNICATION_FAILED:
+        ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
+        break;
+      case MEASUREMENT_INIT_FAILED:
+        ESP_LOGW(TAG, "Measurement Initialization failed!");
+        break;
+      case FIRMWARE_IDENTIFICATION_FAILED:
+        ESP_LOGW(TAG, "Unable to read sensor firmware version");
+        break;
+      default:
+        ESP_LOGW(TAG, "Unknown setup error!");
+        break;
+    }
+  }
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
+}
+
+void SCD30Component::update() {
+  /// Check if measurement is ready before reading the value
+  if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) {
+    this->status_set_warning();
+    return;
+  }
+
+  uint16_t raw_read_status[1];
+  if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) {
+    this->status_set_warning();
+    ESP_LOGW(TAG, "Data not ready yet!");
+    return;
+  }
+
+  if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) {
+    ESP_LOGW(TAG, "Error reading measurement!");
+    this->status_set_warning();
+    return;
+  }
+
+  this->set_timeout(50, [this]() {
+    uint16_t raw_data[6];
+    if (!this->read_data_(raw_data, 6)) {
+      this->status_set_warning();
+      return;
+    }
+
+    union uint32_float_t {
+      uint32_t uint32;
+      float value;
+    };
+    uint32_t temp_c_o2_u32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1])));
+    uint32_float_t co2{.uint32 = temp_c_o2_u32};
+
+    uint32_t temp_temp_u32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3])));
+    uint32_float_t temperature{.uint32 = temp_temp_u32};
+
+    uint32_t temp_hum_u32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5])));
+    uint32_float_t humidity{.uint32 = temp_hum_u32};
+
+    ESP_LOGD(TAG, "Got CO2=%.2fppm temperature=%.2f°C humidity=%.2f%%", co2.value, temperature.value, humidity.value);
+    if (this->co2_sensor_ != nullptr)
+      this->co2_sensor_->publish_state(co2.value);
+    if (this->temperature_sensor_ != nullptr)
+      this->temperature_sensor_->publish_state(temperature.value);
+    if (this->humidity_sensor_ != nullptr)
+      this->humidity_sensor_->publish_state(humidity.value);
+
+    this->status_clear_warning();
+  });
+}
+
+bool SCD30Component::write_command_(uint16_t command) {
+  // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
+  return this->write_byte(command >> 8, command & 0xFF);
+}
+
+uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) {
+  uint8_t bit;
+  uint8_t crc = 0xFF;
+
+  crc ^= data1;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  crc ^= data2;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  return crc;
+}
+
+bool SCD30Component::read_data_(uint16_t *data, uint8_t len) {
+  const uint8_t num_bytes = len * 3;
+  auto *buf = new uint8_t[num_bytes];
+
+  if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) {
+    delete[](buf);
+    return false;
+  }
+
+  for (uint8_t i = 0; i < len; i++) {
+    const uint8_t j = 3 * i;
+    uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
+    if (crc != buf[j + 2]) {
+      ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
+      delete[](buf);
+      return false;
+    }
+    data[i] = (buf[j] << 8) | buf[j + 1];
+  }
+
+  delete[](buf);
+  return true;
+}
+
+}  // namespace scd30
+}  // namespace esphome
diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h
new file mode 100644
index 0000000000..999e66414d
--- /dev/null
+++ b/esphome/components/scd30/scd30.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace scd30 {
+
+/// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors.
+class SCD30Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
+  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
+  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
+
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  bool write_command_(uint16_t command);
+  bool read_data_(uint16_t *data, uint8_t len);
+  uint8_t sht_crc_(uint8_t data1, uint8_t data2);
+
+  enum ErrorCode {
+    COMMUNICATION_FAILED,
+    FIRMWARE_IDENTIFICATION_FAILED,
+    MEASUREMENT_INIT_FAILED,
+    UNKNOWN
+  } error_code_{UNKNOWN};
+
+  sensor::Sensor *co2_sensor_{nullptr};
+  sensor::Sensor *humidity_sensor_{nullptr};
+  sensor::Sensor *temperature_sensor_{nullptr};
+};
+
+}  // namespace scd30
+}  // namespace esphome
diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py
new file mode 100644
index 0000000000..7a60725276
--- /dev/null
+++ b/esphome/components/scd30/sensor.py
@@ -0,0 +1,37 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \
+    CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \
+    UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT, CONF_CO2
+
+DEPENDENCIES = ['i2c']
+
+scd30_ns = cg.esphome_ns.namespace('scd30')
+SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SCD30Component),
+    cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION,
+                                                ICON_PERIODIC_TABLE_CO2, 0),
+    cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+
+    if CONF_CO2 in config:
+        sens = yield sensor.new_sensor(config[CONF_CO2])
+        cg.add(var.set_co2_sensor(sens))
+
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity_sensor(sens))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature_sensor(sens))
diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py
index e1983689a6..9590679f83 100644
--- a/esphome/components/script/__init__.py
+++ b/esphome/components/script/__init__.py
@@ -8,6 +8,7 @@ script_ns = cg.esphome_ns.namespace('script')
 Script = script_ns.class_('Script', automation.Trigger.template())
 ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action)
 ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action)
+ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action)
 IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition)
 
 CONFIG_SCHEMA = automation.validate_automation({
@@ -42,6 +43,14 @@ def script_stop_action_to_code(config, action_id, template_arg, args):
     yield cg.new_Pvariable(action_id, template_arg, paren)
 
 
+@automation.register_action('script.wait', ScriptWaitAction, maybe_simple_id({
+    cv.Required(CONF_ID): cv.use_id(Script)
+}))
+def script_wait_action_to_code(config, action_id, template_arg, args):
+    paren = yield cg.get_variable(config[CONF_ID])
+    yield cg.new_Pvariable(action_id, template_arg, paren)
+
+
 @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({
     cv.Required(CONF_ID): cv.use_id(Script)
 }))
diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h
index f937b9d637..3b97327da8 100644
--- a/esphome/components/script/script.h
+++ b/esphome/components/script/script.h
@@ -49,5 +49,47 @@ template<typename... Ts> class IsRunningCondition : public Condition<Ts...> {
   Script *parent_;
 };
 
+template<typename... Ts> class ScriptWaitAction : public Action<Ts...>, public Component {
+ public:
+  ScriptWaitAction(Script *script) : script_(script) {}
+
+  void play(Ts... x) { /* ignore - see play_complex */
+  }
+
+  void play_complex(Ts... x) override {
+    // Check if we can continue immediately.
+    if (!this->script_->is_running()) {
+      this->triggered_ = false;
+      this->play_next(x...);
+      return;
+    }
+    this->var_ = std::make_tuple(x...);
+    this->triggered_ = true;
+    this->loop();
+  }
+
+  void stop() override { this->triggered_ = false; }
+
+  void loop() override {
+    if (!this->triggered_)
+      return;
+
+    if (this->script_->is_running())
+      return;
+
+    this->triggered_ = false;
+    this->play_next_tuple(this->var_);
+  }
+
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+  bool is_running() override { return this->triggered_ || this->is_running_next(); }
+
+ protected:
+  Script *script_;
+  bool triggered_{false};
+  std::tuple<Ts...> var_{};
+};
+
 }  // namespace script
 }  // namespace esphome
diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp
index 1abb6210ce..1a5be0adc3 100644
--- a/esphome/components/sds011/sds011.cpp
+++ b/esphome/components/sds011/sds011.cpp
@@ -56,6 +56,7 @@ void SDS011Component::dump_config() {
   ESP_LOGCONFIG(TAG, "  RX-only mode: %s", ONOFF(this->rx_mode_only_));
   LOG_SENSOR("  ", "PM2.5", this->pm_2_5_sensor_);
   LOG_SENSOR("  ", "PM10.0", this->pm_10_0_sensor_);
+  this->check_uart_settings(9600);
 }
 
 void SDS011Component::loop() {
@@ -94,7 +95,6 @@ float SDS011Component::get_setup_priority() const { return setup_priority::DATA;
 void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; }
 
 void SDS011Component::sds011_write_command_(const uint8_t *command_data) {
-  this->flush();
   this->write_byte(SDS011_MSG_HEAD);
   this->write_byte(SDS011_COMMAND_ID_REQUEST);
   this->write_array(command_data, SDS011_DATA_REQUEST_LENGTH);
diff --git a/esphome/components/senseair/__init__.py b/esphome/components/senseair/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp
new file mode 100644
index 0000000000..8b41a441ad
--- /dev/null
+++ b/esphome/components/senseair/senseair.cpp
@@ -0,0 +1,80 @@
+#include "senseair.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace senseair {
+
+static const char *TAG = "senseair";
+static const uint8_t SENSEAIR_REQUEST_LENGTH = 8;
+static const uint8_t SENSEAIR_RESPONSE_LENGTH = 13;
+static const uint8_t SENSEAIR_COMMAND_GET_PPM[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6};
+
+void SenseAirComponent::update() {
+  uint8_t response[SENSEAIR_RESPONSE_LENGTH];
+  if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM, response)) {
+    ESP_LOGW(TAG, "Reading data from SenseAir failed!");
+    this->status_set_warning();
+    return;
+  }
+
+  if (response[0] != 0xFE || response[1] != 0x04) {
+    ESP_LOGW(TAG, "Invalid preamble from SenseAir!");
+    this->status_set_warning();
+    return;
+  }
+
+  uint16_t calc_checksum = this->senseair_checksum_(response, 11);
+  uint16_t resp_checksum = (uint16_t(response[12]) << 8) | response[11];
+  if (resp_checksum != calc_checksum) {
+    ESP_LOGW(TAG, "SenseAir checksum doesn't match: 0x%02X!=0x%02X", resp_checksum, calc_checksum);
+    this->status_set_warning();
+    return;
+  }
+
+  this->status_clear_warning();
+  const uint8_t length = response[2];
+  const uint16_t status = (uint16_t(response[3]) << 8) | response[4];
+  const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2];
+
+  ESP_LOGD(TAG, "SenseAir Received COâ‚‚=%uppm Status=0x%02X", ppm, status);
+  if (this->co2_sensor_ != nullptr)
+    this->co2_sensor_->publish_state(ppm);
+}
+
+uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) {
+  uint16_t crc = 0xFFFF;
+  uint8_t i;
+  while (length--) {
+    crc ^= *ptr++;
+    for (i = 0; i < 8; i++) {
+      if ((crc & 0x01) != 0) {
+        crc >>= 1;
+        crc ^= 0xA001;
+      } else {
+        crc >>= 1;
+      }
+    }
+  }
+  return crc;
+}
+
+bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response) {
+  this->flush();
+  this->write_array(command, SENSEAIR_REQUEST_LENGTH);
+
+  if (response == nullptr)
+    return true;
+
+  bool ret = this->read_array(response, SENSEAIR_RESPONSE_LENGTH);
+  this->flush();
+  return ret;
+}
+
+void SenseAirComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "SenseAir:");
+  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
+  this->check_uart_settings(9600);
+}
+
+}  // namespace senseair
+}  // namespace esphome
diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h
new file mode 100644
index 0000000000..23bcf40b5a
--- /dev/null
+++ b/esphome/components/senseair/senseair.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/uart/uart.h"
+
+namespace esphome {
+namespace senseair {
+
+class SenseAirComponent : public PollingComponent, public uart::UARTDevice {
+ public:
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
+
+  void update() override;
+  void dump_config() override;
+
+ protected:
+  uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length);
+  bool senseair_write_command_(const uint8_t *command, uint8_t *response);
+
+  sensor::Sensor *co2_sensor_{nullptr};
+};
+
+}  // namespace senseair
+}  // namespace esphome
diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py
new file mode 100644
index 0000000000..393bfd5182
--- /dev/null
+++ b/esphome/components/senseair/sensor.py
@@ -0,0 +1,24 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, uart
+from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION
+
+DEPENDENCIES = ['uart']
+
+senseair_ns = cg.esphome_ns.namespace('senseair')
+SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, uart.UARTDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SenseAirComponent),
+    cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0),
+}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield uart.register_uart_device(var, config)
+
+    if CONF_CO2 in config:
+        sens = yield sensor.new_sensor(config[CONF_CO2])
+        cg.add(var.set_co2_sensor(sens))
diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py
index 43f0cefd56..605f72a103 100644
--- a/esphome/components/sensor/__init__.py
+++ b/esphome/components/sensor/__init__.py
@@ -6,10 +6,9 @@ from esphome import automation
 from esphome.components import mqtt
 from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, \
     CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, CONF_ICON, CONF_ID, CONF_INTERNAL, \
-    CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, \
-    CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_TO, CONF_TRIGGER_ID, \
-    CONF_UNIT_OF_MEASUREMENT, \
-    CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID
+    CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, \
+    CONF_TO, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID, \
+    CONF_FORCE_UPDATE
 from esphome.core import CORE, coroutine, coroutine_with_priority
 from esphome.util import Registry
 
@@ -61,6 +60,7 @@ SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action)
 
 # Filters
 Filter = sensor_ns.class_('Filter')
+MedianFilter = sensor_ns.class_('MedianFilter', Filter)
 SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter)
 ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter)
 LambdaFilter = sensor_ns.class_('LambdaFilter', Filter)
@@ -73,6 +73,7 @@ HeartbeatFilter = sensor_ns.class_('HeartbeatFilter', Filter, cg.Component)
 DeltaFilter = sensor_ns.class_('DeltaFilter', Filter)
 OrFilter = sensor_ns.class_('OrFilter', Filter)
 CalibrateLinearFilter = sensor_ns.class_('CalibrateLinearFilter', Filter)
+CalibratePolynomialFilter = sensor_ns.class_('CalibratePolynomialFilter', Filter)
 SensorInRangeCondition = sensor_ns.class_('SensorInRangeCondition', Filter)
 
 unit_of_measurement = cv.string_strict
@@ -85,6 +86,7 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({
     cv.Optional(CONF_UNIT_OF_MEASUREMENT): unit_of_measurement,
     cv.Optional(CONF_ICON): icon,
     cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals,
+    cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean,
     cv.Optional(CONF_EXPIRE_AFTER): cv.All(cv.requires_component('mqtt'),
                                            cv.Any(None, cv.positive_time_period_milliseconds)),
     cv.Optional(CONF_FILTERS): validate_filters,
@@ -126,6 +128,19 @@ def filter_out_filter_to_code(config, filter_id):
     yield cg.new_Pvariable(filter_id, config)
 
 
+MEDIAN_SCHEMA = cv.All(cv.Schema({
+    cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
+    cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
+    cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
+}), validate_send_first_at)
+
+
+@FILTER_REGISTRY.register('median', MedianFilter, MEDIAN_SCHEMA)
+def median_filter_to_code(config, filter_id):
+    yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY],
+                           config[CONF_SEND_FIRST_AT])
+
+
 SLIDING_AVERAGE_SCHEMA = cv.All(cv.Schema({
     cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int,
     cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
@@ -185,8 +200,15 @@ def debounce_filter_to_code(config, filter_id):
     yield var
 
 
+def validate_not_all_from_same(config):
+    if all(conf[CONF_FROM] == config[0][CONF_FROM] for conf in config):
+        raise cv.Invalid("The 'from' values of the calibrate_linear filter cannot all point "
+                         "to the same value! Please add more values to the filter.")
+    return config
+
+
 @FILTER_REGISTRY.register('calibrate_linear', CalibrateLinearFilter, cv.All(
-    cv.ensure_list(validate_datapoint), cv.Length(min=2)))
+    cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same))
 def calibrate_linear_filter_to_code(config, filter_id):
     x = [conf[CONF_FROM] for conf in config]
     y = [conf[CONF_TO] for conf in config]
@@ -194,6 +216,32 @@ def calibrate_linear_filter_to_code(config, filter_id):
     yield cg.new_Pvariable(filter_id, k, b)
 
 
+CONF_DATAPOINTS = 'datapoints'
+CONF_DEGREE = 'degree'
+
+
+def validate_calibrate_polynomial(config):
+    if config[CONF_DEGREE] >= len(config[CONF_DATAPOINTS]):
+        raise cv.Invalid("Degree is too high! Maximum possible degree with given datapoints is "
+                         "{}".format(len(config[CONF_DATAPOINTS]) - 1), [CONF_DEGREE])
+    return config
+
+
+@FILTER_REGISTRY.register('calibrate_polynomial', CalibratePolynomialFilter, cv.All(cv.Schema({
+    cv.Required(CONF_DATAPOINTS): cv.All(cv.ensure_list(validate_datapoint), cv.Length(min=1)),
+    cv.Required(CONF_DEGREE): cv.positive_int,
+}), validate_calibrate_polynomial))
+def calibrate_polynomial_filter_to_code(config, filter_id):
+    x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]]
+    y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]]
+    degree = config[CONF_DEGREE]
+    a = [[1] + [x_**(i+1) for i in range(degree)] for x_ in x]
+    # Column vector
+    b = [[v] for v in y]
+    res = [v[0] for v in _lstsq(a, b)]
+    yield cg.new_Pvariable(filter_id, res)
+
+
 @coroutine
 def build_filters(config):
     yield cg.build_registry_list(FILTER_REGISTRY, config)
@@ -210,7 +258,8 @@ 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_FILTERS in config:
+    cg.add(var.set_force_update(config[CONF_FORCE_UPDATE]))
+    if config.get(CONF_FILTERS):  # must exist and not be empty
         filters = yield build_filters(config[CONF_FILTERS])
         cg.add(var.set_filters(filters))
 
@@ -303,6 +352,66 @@ def fit_linear(x, y):
     return k, b
 
 
+def _mat_copy(m):
+    return [list(row) for row in m]
+
+
+def _mat_transpose(m):
+    return _mat_copy(zip(*m))
+
+
+def _mat_identity(n):
+    return [[int(i == j) for j in range(n)] for i in range(n)]
+
+
+def _mat_dot(a, b):
+    b_t = _mat_transpose(b)
+    return [[sum(x*y for x, y in zip(row_a, col_b)) for col_b in b_t] for row_a in a]
+
+
+def _mat_inverse(m):
+    n = len(m)
+    m = _mat_copy(m)
+    id = _mat_identity(n)
+
+    for diag in range(n):
+        # If diag element is 0, swap rows
+        if m[diag][diag] == 0:
+            for i in range(diag+1, n):
+                if m[i][diag] != 0:
+                    break
+            else:
+                raise ValueError("Singular matrix, inverse cannot be calculated!")
+
+            # Swap rows
+            m[diag], m[i] = m[i], m[diag]
+            id[diag], id[i] = id[i], id[diag]
+
+        # Scale row to 1 in diagonal
+        scaler = 1.0 / m[diag][diag]
+        for j in range(n):
+            m[diag][j] *= scaler
+            id[diag][j] *= scaler
+
+        # Subtract diag row
+        for i in range(n):
+            if i == diag:
+                continue
+            scaler = m[i][diag]
+            for j in range(n):
+                m[i][j] -= scaler * m[diag][j]
+                id[i][j] -= scaler * id[diag][j]
+
+    return id
+
+
+def _lstsq(a, b):
+    # min_x ||b - ax||^2_2 => x = (a^T a)^{-1} a^T b
+    a_t = _mat_transpose(a)
+    x = _mat_inverse(_mat_dot(a_t, a))
+    return _mat_dot(_mat_dot(x, a_t), b)
+
+
 @coroutine_with_priority(40.0)
 def to_code(config):
     cg.add_define('USE_SENSOR')
diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp
index 306607dfda..f7a5b5d7ad 100644
--- a/esphome/components/sensor/filter.cpp
+++ b/esphome/components/sensor/filter.cpp
@@ -39,6 +39,44 @@ uint32_t Filter::calculate_remaining_interval(uint32_t input) {
   }
 }
 
+// MedianFilter
+MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at)
+    : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
+void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
+void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
+optional<float> MedianFilter::new_value(float value) {
+  if (!isnan(value)) {
+    while (this->queue_.size() >= this->window_size_) {
+      this->queue_.pop_front();
+    }
+    this->queue_.push_back(value);
+    ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value);
+  }
+
+  if (++this->send_at_ >= this->send_every_) {
+    this->send_at_ = 0;
+
+    float median = 0.0f;
+    if (!this->queue_.empty()) {
+      std::deque<float> median_queue = this->queue_;
+      sort(median_queue.begin(), median_queue.end());
+
+      size_t queue_size = median_queue.size();
+      if (queue_size % 2) {
+        median = median_queue[queue_size / 2];
+      } else {
+        median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f;
+      }
+    }
+
+    ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING", this, median);
+    return median;
+  }
+  return {};
+}
+
+uint32_t MedianFilter::expected_interval(uint32_t input) { return input * this->send_every_; }
+
 // SlidingWindowMovingAverageFilter
 SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every,
                                                                    size_t send_first_at)
@@ -128,7 +166,11 @@ optional<float> FilterOutValueFilter::new_value(float value) {
     else
       return value;
   } else {
-    if (value == this->value_to_filter_out_)
+    int8_t accuracy = this->parent_->get_accuracy_decimals();
+    float accuracy_mult = pow10f(accuracy);
+    float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_);
+    float rounded_value = roundf(accuracy_mult * value);
+    if (rounded_filter_out == rounded_value)
       return {};
     else
       return value;
@@ -228,5 +270,15 @@ float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDW
 optional<float> CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; }
 CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {}
 
+optional<float> CalibratePolynomialFilter::new_value(float value) {
+  float res = 0.0f;
+  float x = 1.0f;
+  for (float coefficient : this->coefficients_) {
+    res += x * coefficient;
+    x *= value;
+  }
+  return res;
+}
+
 }  // namespace sensor
 }  // namespace esphome
diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h
index 6bd22be230..4c61d4c0a2 100644
--- a/esphome/components/sensor/filter.h
+++ b/esphome/components/sensor/filter.h
@@ -46,6 +46,36 @@ class Filter {
   Sensor *parent_{nullptr};
 };
 
+/** Simple median filter.
+ *
+ * Takes the median of the last <send_every> values and pushes it out every <send_every>.
+ */
+class MedianFilter : public Filter {
+ public:
+  /** Construct a MedianFilter.
+   *
+   * @param window_size The number of values that should be used in median calculation.
+   * @param send_every After how many sensor values should a new one be pushed out.
+   * @param send_first_at After how many values to forward the very first value. Defaults to the first value
+   *   on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
+   *   send_every.
+   */
+  explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at);
+
+  optional<float> new_value(float value) override;
+
+  void set_send_every(size_t send_every);
+  void set_window_size(size_t window_size);
+
+  uint32_t expected_interval(uint32_t input) override;
+
+ protected:
+  std::deque<float> queue_;
+  size_t send_every_;
+  size_t send_at_;
+  size_t window_size_;
+};
+
 /** Simple sliding window moving average filter.
  *
  * Essentially just takes takes the average of the last window_size values and pushes them out
@@ -243,5 +273,14 @@ class CalibrateLinearFilter : public Filter {
   float bias_;
 };
 
+class CalibratePolynomialFilter : public Filter {
+ public:
+  CalibratePolynomialFilter(const std::vector<float> &coefficients) : coefficients_(coefficients) {}
+  optional<float> new_value(float value) override;
+
+ protected:
+  std::vector<float> coefficients_;
+};
+
 }  // namespace sensor
 }  // namespace esphome
diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h
index 1c7c854394..f23f022767 100644
--- a/esphome/components/sensor/sensor.h
+++ b/esphome/components/sensor/sensor.h
@@ -9,14 +9,17 @@ namespace sensor {
 
 #define LOG_SENSOR(prefix, type, obj) \
   if (obj != nullptr) { \
-    ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \
-    ESP_LOGCONFIG(TAG, prefix "  Unit of Measurement: '%s'", obj->get_unit_of_measurement().c_str()); \
-    ESP_LOGCONFIG(TAG, prefix "  Accuracy Decimals: %d", obj->get_accuracy_decimals()); \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
+    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()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Icon: '%s'", obj->get_icon().c_str()); \
+      ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, obj->get_icon().c_str()); \
     } \
     if (!obj->unique_id().empty()) { \
-      ESP_LOGV(TAG, prefix "  Unique ID: '%s'", obj->unique_id().c_str()); \
+      ESP_LOGV(TAG, "%s  Unique ID: '%s'", prefix, obj->unique_id().c_str()); \
+    } \
+    if (obj->get_force_update()) { \
+      ESP_LOGV(TAG, "%s  Force Update: YES", prefix); \
     } \
   }
 
@@ -142,6 +145,15 @@ class Sensor : public Nameable {
 
   void internal_send_state_to_frontend(float state);
 
+  bool get_force_update() const { return force_update_; }
+  /** Set this sensor's force_update mode.
+   *
+   * If the sensor is in force_update mode, the frontend is required to save all
+   * state changes to the database when they are published, even if the state is the
+   * same as before.
+   */
+  void set_force_update(bool force_update) { force_update_ = force_update; }
+
  protected:
   /** Override this to set the Home Assistant unit of measurement for this sensor.
    *
@@ -174,6 +186,7 @@ class Sensor : public Nameable {
   optional<int8_t> accuracy_decimals_;
   Filter *filter_list_{nullptr};  ///< Store all active filters.
   bool has_state_{false};
+  bool force_update_{false};
 };
 
 class PollingSensorComponent : public PollingComponent, public Sensor {
diff --git a/esphome/components/sgp30/__init__.py b/esphome/components/sgp30/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py
new file mode 100644
index 0000000000..6329b122fd
--- /dev/null
+++ b/esphome/components/sgp30/sensor.py
@@ -0,0 +1,54 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \
+    UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2
+
+DEPENDENCIES = ['i2c']
+
+sgp30_ns = cg.esphome_ns.namespace('sgp30')
+SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CDevice)
+
+CONF_ECO2 = 'eco2'
+CONF_TVOC = 'tvoc'
+CONF_BASELINE = 'baseline'
+CONF_UPTIME = 'uptime'
+CONF_COMPENSATION = 'compensation'
+CONF_HUMIDITY_SOURCE = 'humidity_source'
+CONF_TEMPERATURE_SOURCE = 'temperature_source'
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SGP30Component),
+    cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION,
+                                                 ICON_PERIODIC_TABLE_CO2, 0),
+    cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0),
+    cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
+    cv.Optional(CONF_COMPENSATION): cv.Schema({
+        cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
+        cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor)
+    }),
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x58))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+
+    if CONF_ECO2 in config:
+        sens = yield sensor.new_sensor(config[CONF_ECO2])
+        cg.add(var.set_eco2_sensor(sens))
+
+    if CONF_TVOC in config:
+        sens = yield sensor.new_sensor(config[CONF_TVOC])
+        cg.add(var.set_tvoc_sensor(sens))
+
+    if CONF_BASELINE in config:
+        cg.add(var.set_baseline(config[CONF_BASELINE]))
+
+    if CONF_COMPENSATION in config:
+        compensation_config = config[CONF_COMPENSATION]
+        sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
+        cg.add(var.set_humidity_sensor(sens))
+        sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
+        cg.add(var.set_temperature_sensor(sens))
diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp
new file mode 100644
index 0000000000..9a73295447
--- /dev/null
+++ b/esphome/components/sgp30/sgp30.cpp
@@ -0,0 +1,295 @@
+#include "sgp30.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sgp30 {
+
+static const char *TAG = "sgp30";
+
+static const uint16_t SGP30_CMD_GET_SERIAL_ID = 0x3682;
+static const uint16_t SGP30_CMD_GET_FEATURESET = 0x202f;
+static const uint16_t SGP30_CMD_IAQ_INIT = 0x2003;
+static const uint16_t SGP30_CMD_MEASURE_IAQ = 0x2008;
+static const uint16_t SGP30_CMD_SET_ABSOLUTE_HUMIDITY = 0x2061;
+static const uint16_t SGP30_CMD_GET_IAQ_BASELINE = 0x2015;
+static const uint16_t SGP30_CMD_SET_IAQ_BASELINE = 0x201E;
+
+// Sensor baseline should first be relied on after 1H of operation,
+// if the sensor starts with a baseline value provided
+const long IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600;
+
+// Sensor baseline could first be relied on after 12H of operation,
+// if the sensor starts without any prior baseline value provided
+const long IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200;
+
+void SGP30Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up SGP30...");
+
+  // Serial Number identification
+  if (!this->write_command_(SGP30_CMD_GET_SERIAL_ID)) {
+    this->error_code_ = COMMUNICATION_FAILED;
+    this->mark_failed();
+    return;
+  }
+  uint16_t raw_serial_number[3];
+
+  if (!this->read_data_(raw_serial_number, 3)) {
+    this->mark_failed();
+    return;
+  }
+  this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
+                         (uint64_t(raw_serial_number[2]));
+  ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_);
+
+  // Featureset identification for future use
+  if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) {
+    this->mark_failed();
+    return;
+  }
+  uint16_t raw_featureset[1];
+  if (!this->read_data_(raw_featureset, 1)) {
+    this->mark_failed();
+    return;
+  }
+  this->featureset_ = raw_featureset[0];
+  if (uint16_t(this->featureset_ >> 12) != 0x0) {
+    if (uint16_t(this->featureset_ >> 12) == 0x1) {
+      // ID matching a different sensor: SGPC3
+      this->error_code_ = UNSUPPORTED_ID;
+    } else {
+      // Unknown ID
+      this->error_code_ = INVALID_ID;
+    }
+    this->mark_failed();
+    return;
+  }
+  ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
+
+  // Sensor initialization
+  if (!this->write_command_(SGP30_CMD_IAQ_INIT)) {
+    ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed.");
+    this->error_code_ = MEASUREMENT_INIT_FAILED;
+    this->mark_failed();
+    return;
+  }
+
+  // Sensor baseline reliability timer
+  if (this->baseline_ > 0) {
+    this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED;
+    this->write_iaq_baseline_(this->baseline_);
+  } else {
+    this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE;
+  }
+}
+
+bool SGP30Component::is_sensor_baseline_reliable_() {
+  if ((this->required_warm_up_time_ == 0) || (std::floor(millis() / 1000) >= this->required_warm_up_time_)) {
+    // requirement for warm up is removed once the millis uptime surpasses the required warm_up_time
+    // this avoids the repetitive warm up when the millis uptime is rolled over every ~40 days
+    this->required_warm_up_time_ = 0;
+    return true;
+  }
+  return false;
+}
+
+void SGP30Component::read_iaq_baseline_() {
+  if (this->is_sensor_baseline_reliable_()) {
+    if (!this->write_command_(SGP30_CMD_GET_IAQ_BASELINE)) {
+      ESP_LOGD(TAG, "Error getting baseline");
+      this->status_set_warning();
+      return;
+    }
+    this->set_timeout(50, [this]() {
+      uint16_t raw_data[2];
+      if (!this->read_data_(raw_data, 2)) {
+        this->status_set_warning();
+        return;
+      }
+
+      uint8_t eco2baseline = (raw_data[0]);
+      uint8_t tvocbaseline = (raw_data[1]);
+
+      ESP_LOGI(TAG, "Current eCO2 & TVOC baseline: 0x%04X", uint16_t((eco2baseline << 8) | (tvocbaseline & 0xFF)));
+      this->status_clear_warning();
+    });
+  } else {
+    ESP_LOGD(TAG, "Baseline reading not available for: %.0fs",
+             (this->required_warm_up_time_ - std::floor(millis() / 1000)));
+  }
+}
+
+void SGP30Component::send_env_data_() {
+  if (this->humidity_sensor_ == nullptr && this->temperature_sensor_ == nullptr)
+    return;
+  float humidity = NAN;
+  if (this->humidity_sensor_ != nullptr)
+    humidity = this->humidity_sensor_->state;
+  if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
+    ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data.");
+    return;
+  } else {
+    ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity);
+  }
+  float temperature = NAN;
+  if (this->temperature_sensor_ != nullptr) {
+    temperature = float(this->temperature_sensor_->state);
+  }
+  if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
+    ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data.");
+    return;
+  } else {
+    ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature);
+  }
+
+  float absolute_humidity =
+      216.7f * (((humidity / 100) * 6.112f * std::exp((17.62f * temperature) / (243.12f + temperature))) /
+                (273.15f + temperature));
+  uint8_t humidity_full = uint8_t(std::floor(absolute_humidity));
+  uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256));
+  ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity,
+           uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec)));
+  uint8_t crc = sht_crc_(humidity_full, humidity_dec);
+  uint8_t data[4];
+  data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF;
+  data[1] = humidity_full;
+  data[2] = humidity_dec;
+  data[3] = crc;
+  if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) {
+    ESP_LOGE(TAG, "Error sending compensation data.");
+  }
+}
+
+void SGP30Component::write_iaq_baseline_(uint16_t baseline) {
+  uint8_t e_c_o2_baseline = baseline >> 8;
+  uint8_t tvoc_baseline = baseline & 0xFF;
+  uint8_t data[4];
+  data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF;
+  data[1] = e_c_o2_baseline;
+  data[2] = tvoc_baseline;
+  data[3] = sht_crc_(e_c_o2_baseline, tvoc_baseline);
+  if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 4)) {
+    ESP_LOGE(TAG, "Error applying baseline: 0x%04X", baseline);
+  } else
+    ESP_LOGI(TAG, "Initial baseline 0x%04X applied successfully!", baseline);
+}
+
+void SGP30Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "SGP30:");
+  LOG_I2C_DEVICE(this);
+  if (this->is_failed()) {
+    switch (this->error_code_) {
+      case COMMUNICATION_FAILED:
+        ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
+        break;
+      case MEASUREMENT_INIT_FAILED:
+        ESP_LOGW(TAG, "Measurement Initialization failed!");
+        break;
+      case INVALID_ID:
+        ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?");
+        break;
+      case UNSUPPORTED_ID:
+        ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3).");
+        break;
+      default:
+        ESP_LOGW(TAG, "Unknown setup error!");
+        break;
+    }
+  } else {
+    ESP_LOGCONFIG(TAG, "  Serial number: %llu", this->serial_number_);
+    ESP_LOGCONFIG(TAG, "  Baseline: 0x%04X%s", this->baseline_,
+                  ((this->baseline_ != 0x0000) ? " (enabled)" : " (disabled)"));
+    ESP_LOGCONFIG(TAG, "  Warm up time: %lds", this->required_warm_up_time_);
+  }
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "eCO2", this->eco2_sensor_);
+  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
+  if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
+    ESP_LOGCONFIG(TAG, "  Compensation:");
+    LOG_SENSOR("    ", "Temperature Source:", this->temperature_sensor_);
+    LOG_SENSOR("    ", "Humidity Source:", this->humidity_sensor_);
+  } else {
+    ESP_LOGCONFIG(TAG, "  Compensation: No source configured");
+  }
+}
+
+void SGP30Component::update() {
+  if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) {
+    this->status_set_warning();
+    return;
+  }
+
+  this->set_timeout(50, [this]() {
+    uint16_t raw_data[2];
+    if (!this->read_data_(raw_data, 2)) {
+      this->status_set_warning();
+      return;
+    }
+
+    float eco2 = (raw_data[0]);
+    float tvoc = (raw_data[1]);
+
+    ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc);
+    if (this->eco2_sensor_ != nullptr)
+      this->eco2_sensor_->publish_state(eco2);
+    if (this->tvoc_sensor_ != nullptr)
+      this->tvoc_sensor_->publish_state(tvoc);
+    this->status_clear_warning();
+    this->send_env_data_();
+    this->read_iaq_baseline_();
+  });
+}
+
+bool SGP30Component::write_command_(uint16_t command) {
+  // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
+  return this->write_byte(command >> 8, command & 0xFF);
+}
+
+uint8_t SGP30Component::sht_crc_(uint8_t data1, uint8_t data2) {
+  uint8_t bit;
+  uint8_t crc = 0xFF;
+
+  crc ^= data1;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  crc ^= data2;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  return crc;
+}
+
+bool SGP30Component::read_data_(uint16_t *data, uint8_t len) {
+  const uint8_t num_bytes = len * 3;
+  auto *buf = new uint8_t[num_bytes];
+
+  if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) {
+    delete[](buf);
+    return false;
+  }
+
+  for (uint8_t i = 0; i < len; i++) {
+    const uint8_t j = 3 * i;
+    uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
+    if (crc != buf[j + 2]) {
+      ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
+      delete[](buf);
+      return false;
+    }
+    data[i] = (buf[j] << 8) | buf[j + 1];
+  }
+
+  delete[](buf);
+  return true;
+}
+
+}  // namespace sgp30
+}  // namespace esphome
diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h
new file mode 100644
index 0000000000..2362d1bca6
--- /dev/null
+++ b/esphome/components/sgp30/sgp30.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+#include <cmath>
+
+namespace esphome {
+namespace sgp30 {
+
+/// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors.
+class SGP30Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; }
+  void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
+  void set_baseline(uint16_t baseline) { baseline_ = baseline; }
+  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
+  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
+
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  bool write_command_(uint16_t command);
+  bool read_data_(uint16_t *data, uint8_t len);
+  void send_env_data_();
+  void read_iaq_baseline_();
+  bool is_sensor_baseline_reliable_();
+  void write_iaq_baseline_(uint16_t baseline);
+  uint8_t sht_crc_(uint8_t data1, uint8_t data2);
+  uint64_t serial_number_;
+  uint16_t featureset_;
+  long required_warm_up_time_;
+
+  enum ErrorCode {
+    COMMUNICATION_FAILED,
+    MEASUREMENT_INIT_FAILED,
+    INVALID_ID,
+    UNSUPPORTED_ID,
+    UNKNOWN
+  } error_code_{UNKNOWN};
+
+  sensor::Sensor *eco2_sensor_{nullptr};
+  sensor::Sensor *tvoc_sensor_{nullptr};
+  uint16_t baseline_{0x0000};
+  /// Input sensor for humidity and temperature compensation.
+  sensor::Sensor *humidity_sensor_{nullptr};
+  sensor::Sensor *temperature_sensor_{nullptr};
+};
+
+}  // namespace sgp30
+}  // namespace esphome
diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp
index f23c0d59b4..559fdc21ab 100644
--- a/esphome/components/sht3xd/sht3xd.cpp
+++ b/esphome/components/sht3xd/sht3xd.cpp
@@ -43,8 +43,14 @@ void SHT3XDComponent::dump_config() {
 }
 float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; }
 void SHT3XDComponent::update() {
-  if (!this->write_command_(SHT3XD_COMMAND_POLLING_H))
+  if (this->status_has_warning()) {
+    ESP_LOGD(TAG, "Retrying to reconnect the sensor.");
+    this->write_command_(SHT3XD_COMMAND_SOFT_RESET);
+  }
+  if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) {
+    this->status_set_warning();
     return;
+  }
 
   this->set_timeout(50, [this]() {
     uint16_t raw_data[2];
diff --git a/esphome/components/shtcx/__init__.py b/esphome/components/shtcx/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py
new file mode 100644
index 0000000000..eb215078e7
--- /dev/null
+++ b/esphome/components/shtcx/sensor.py
@@ -0,0 +1,32 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, ICON_WATER_PERCENT, \
+    ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT
+
+DEPENDENCIES = ['i2c']
+
+shtcx_ns = cg.esphome_ns.namespace('shtcx')
+SHTCXComponent = shtcx_ns.class_('SHTCXComponent', cg.PollingComponent, i2c.I2CDevice)
+
+SHTCXType = shtcx_ns.enum('SHTCXType')
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SHTCXComponent),
+    cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x70))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature_sensor(sens))
+
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp
new file mode 100644
index 0000000000..b8daceb1af
--- /dev/null
+++ b/esphome/components/shtcx/shtcx.cpp
@@ -0,0 +1,166 @@
+#include "shtcx.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace shtcx {
+
+static const char *TAG = "shtcx";
+
+static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098;
+static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517;
+static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8;
+static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D;
+static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866;
+
+inline const char *to_string(SHTCXType type) {
+  switch (type) {
+    case SHTCX_TYPE_SHTC3:
+      return "SHTC3";
+    case SHTCX_TYPE_SHTC1:
+      return "SHTC1";
+    default:
+      return "[Unknown model]";
+  }
+}
+
+void SHTCXComponent::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up SHTCx...");
+  this->soft_reset();
+
+  if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) {
+    ESP_LOGE(TAG, "Error requesting Device ID");
+    this->mark_failed();
+    return;
+  }
+
+  uint16_t device_id_register[1];
+  if (!this->read_data_(device_id_register, 1)) {
+    ESP_LOGE(TAG, "Error reading Device ID");
+    this->mark_failed();
+    return;
+  }
+
+  if (((device_id_register[0] << 2) & 0x1C) == 0x1C) {
+    if ((device_id_register[0] & 0x847) == 0x847) {
+      this->type_ = SHTCX_TYPE_SHTC3;
+    } else {
+      this->type_ = SHTCX_TYPE_SHTC1;
+    }
+  } else {
+    this->type_ = SHTCX_TYPE_UNKNOWN;
+  }
+  ESP_LOGCONFIG(TAG, "  Device identified: %s", to_string(this->type_));
+}
+void SHTCXComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "SHTCx:");
+  ESP_LOGCONFIG(TAG, "  Model: %s", to_string(this->type_));
+  LOG_I2C_DEVICE(this);
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Communication with SHTCx failed!");
+  }
+  LOG_UPDATE_INTERVAL(this);
+
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
+}
+float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; }
+void SHTCXComponent::update() {
+  if (this->status_has_warning()) {
+    ESP_LOGW(TAG, "Retrying to reconnect the sensor.");
+    this->soft_reset();
+  }
+  if (this->type_ != SHTCX_TYPE_SHTC1) {
+    this->wake_up();
+  }
+  if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) {
+    this->status_set_warning();
+    return;
+  }
+
+  this->set_timeout(50, [this]() {
+    uint16_t raw_data[2];
+    if (!this->read_data_(raw_data, 2)) {
+      this->status_set_warning();
+      return;
+    }
+
+    float temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f;
+    float humidity = 100.0f * float(raw_data[1]) / 65536.0f;
+
+    ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
+    if (this->temperature_sensor_ != nullptr)
+      this->temperature_sensor_->publish_state(temperature);
+    if (this->humidity_sensor_ != nullptr)
+      this->humidity_sensor_->publish_state(humidity);
+    this->status_clear_warning();
+    if (this->type_ != SHTCX_TYPE_SHTC1) {
+      this->sleep();
+    }
+  });
+}
+
+bool SHTCXComponent::write_command_(uint16_t command) {
+  // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
+  return this->write_byte(command >> 8, command & 0xFF);
+}
+
+uint8_t sht_crc(uint8_t data1, uint8_t data2) {
+  uint8_t bit;
+  uint8_t crc = 0xFF;
+
+  crc ^= data1;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  crc ^= data2;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  return crc;
+}
+
+bool SHTCXComponent::read_data_(uint16_t *data, uint8_t len) {
+  const uint8_t num_bytes = len * 3;
+  auto *buf = new uint8_t[num_bytes];
+
+  if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) {
+    delete[](buf);
+    return false;
+  }
+
+  for (uint8_t i = 0; i < len; i++) {
+    const uint8_t j = 3 * i;
+    uint8_t crc = sht_crc(buf[j], buf[j + 1]);
+    if (crc != buf[j + 2]) {
+      ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
+      delete[](buf);
+      return false;
+    }
+    data[i] = (buf[j] << 8) | buf[j + 1];
+  }
+
+  delete[](buf);
+  return true;
+}
+
+void SHTCXComponent::soft_reset() {
+  this->write_command_(SHTCX_COMMAND_SOFT_RESET);
+  delayMicroseconds(200);
+}
+void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); }
+
+void SHTCXComponent::wake_up() {
+  this->write_command_(SHTCX_COMMAND_WAKEUP);
+  delayMicroseconds(200);
+}
+
+}  // namespace shtcx
+}  // namespace esphome
diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h
new file mode 100644
index 0000000000..ccc6533bfa
--- /dev/null
+++ b/esphome/components/shtcx/shtcx.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace shtcx {
+
+enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN };
+
+/// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors.
+class SHTCXComponent : public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
+
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void update() override;
+  void soft_reset();
+  void sleep();
+  void wake_up();
+
+ protected:
+  bool write_command_(uint16_t command);
+  bool read_data_(uint16_t *data, uint8_t len);
+  SHTCXType type_;
+  sensor::Sensor *temperature_sensor_;
+  sensor::Sensor *humidity_sensor_;
+};
+
+}  // namespace shtcx
+}  // namespace esphome
diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/shutdown_switch.cpp
index d27bb8aadc..ce33cd187f 100644
--- a/esphome/components/shutdown/shutdown_switch.cpp
+++ b/esphome/components/shutdown/shutdown_switch.cpp
@@ -14,7 +14,7 @@ void ShutdownSwitch::write_state(bool state) {
 
   if (state) {
     ESP_LOGI(TAG, "Shutting down...");
-    delay(100);  // Let MQTT settle a bit
+    delay(100);  // NOLINT
 
     App.run_safe_shutdown_hooks();
 #ifdef ARDUINO_ARCH_ESP8266
diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py
new file mode 100644
index 0000000000..c64112570a
--- /dev/null
+++ b/esphome/components/sim800l/__init__.py
@@ -0,0 +1,59 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.const import CONF_ID, CONF_TRIGGER_ID
+from esphome.components import uart
+
+DEPENDENCIES = ['uart']
+
+sim800l_ns = cg.esphome_ns.namespace('sim800l')
+Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component)
+
+Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger',
+                                                  automation.Trigger.template(cg.std_string,
+                                                                              cg.std_string))
+
+# Actions
+Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action)
+
+MULTI_CONF = True
+
+CONF_ON_SMS_RECEIVED = 'on_sms_received'
+CONF_RECIPIENT = 'recipient'
+CONF_MESSAGE = 'message'
+
+CONFIG_SCHEMA = cv.All(cv.Schema({
+    cv.GenerateID(): cv.declare_id(Sim800LComponent),
+    cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({
+        cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Sim800LReceivedMessageTrigger),
+    }),
+}).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield uart.register_uart_device(var, config)
+
+    for conf in config.get(CONF_ON_SMS_RECEIVED, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        yield automation.build_automation(trigger, [(cg.std_string, 'message'),
+                                                    (cg.std_string, 'sender')], conf)
+
+
+SIM800L_SEND_SMS_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.use_id(Sim800LComponent),
+    cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict),
+    cv.Required(CONF_MESSAGE): cv.templatable(cv.string),
+})
+
+
+@automation.register_action('sim800l.send_sms', Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA)
+def sim800l_send_sms_to_code(config, action_id, template_arg, args):
+    paren = yield cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+    template_ = yield cg.templatable(config[CONF_RECIPIENT], args, cg.std_string)
+    cg.add(var.set_recipient(template_))
+    template_ = yield cg.templatable(config[CONF_MESSAGE], args, cg.std_string)
+    cg.add(var.set_message(template_))
+    yield var
diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp
new file mode 100644
index 0000000000..1390ef8b49
--- /dev/null
+++ b/esphome/components/sim800l/sim800l.cpp
@@ -0,0 +1,264 @@
+#include "sim800l.h"
+#include "esphome/core/log.h"
+#include <string.h>
+
+namespace esphome {
+namespace sim800l {
+
+static const char* TAG = "sim800l";
+
+const char ASCII_CR = 0x0D;
+const char ASCII_LF = 0x0A;
+
+void Sim800LComponent::update() {
+  if (this->watch_dog_++ == 2) {
+    this->state_ = STATE_INIT;
+    this->write(26);
+  }
+
+  if (state_ == STATE_INIT) {
+    if (this->registered_ && this->send_pending_) {
+      this->send_cmd_("AT+CSCS=\"GSM\"");
+      this->state_ = STATE_SENDINGSMS1;
+    } else {
+      this->send_cmd_("AT");
+      this->state_ = STATE_CHECK_AT;
+    }
+    this->expect_ack_ = true;
+  }
+  if (state_ == STATE_RECEIVEDSMS) {
+    // Serial Buffer should have flushed.
+    // Send cmd to delete received sms
+    char delete_cmd[20];
+    sprintf(delete_cmd, "AT+CMGD=%d", this->parse_index_);
+    this->send_cmd_(delete_cmd);
+    this->state_ = STATE_CHECK_SMS;
+    this->expect_ack_ = true;
+  }
+}
+
+void Sim800LComponent::send_cmd_(std::string message) {
+  ESP_LOGV(TAG, "S: %s - %d", message.c_str(), this->state_);
+  this->watch_dog_ = 0;
+  this->write_str(message.c_str());
+  this->write_byte(ASCII_LF);
+}
+
+void Sim800LComponent::parse_cmd_(std::string message) {
+  ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_);
+
+  if (message.empty())
+    return;
+
+  if (this->expect_ack_) {
+    bool ok = message == "OK";
+    this->expect_ack_ = false;
+    if (!ok) {
+      if (this->state_ == STATE_CHECK_AT && message == "AT") {
+        // Expected ack but AT echo received
+        this->state_ = STATE_DISABLE_ECHO;
+        this->expect_ack_ = true;
+      } else {
+        ESP_LOGW(TAG, "Not ack. %d %s", this->state_, message.c_str());
+        this->state_ = STATE_IDLE;  // Let it timeout
+        return;
+      }
+    }
+  }
+
+  switch (this->state_) {
+    case STATE_INIT: {
+      // While we were waiting for update to check for messages, this notifies a message
+      // is available.
+      bool message_available = message.compare(0, 6, "+CMTI:") == 0;
+      if (!message_available)
+        break;
+      // Else fall thru ...
+    }
+    case STATE_CHECK_SMS:
+      send_cmd_("AT+CMGL=\"ALL\"");
+      this->state_ = STATE_PARSE_SMS;
+      this->parse_index_ = 0;
+      break;
+    case STATE_DISABLE_ECHO:
+      send_cmd_("ATE0");
+      this->state_ = STATE_CHECK_AT;
+      this->expect_ack_ = true;
+      break;
+    case STATE_CHECK_AT:
+      send_cmd_("AT+CMGF=1");
+      this->state_ = STATE_CREG;
+      this->expect_ack_ = true;
+      break;
+    case STATE_CREG:
+      send_cmd_("AT+CREG?");
+      this->state_ = STATE_CREGWAIT;
+      break;
+    case STATE_CREGWAIT: {
+      // Response: "+CREG: 0,1" -- the one there means registered ok
+      //           "+CREG: -,-" means not registered ok
+      bool registered = message.compare(0, 6, "+CREG:") == 0 && message[9] == '1';
+      if (registered) {
+        if (!this->registered_)
+          ESP_LOGD(TAG, "Registered OK");
+        this->state_ = STATE_CSQ;
+        this->expect_ack_ = true;
+      } else {
+        ESP_LOGW(TAG, "Registration Fail");
+        if (message[7] == '0') {  // Network registration is disable, enable it
+          send_cmd_("AT+CREG=1");
+          this->expect_ack_ = true;
+          this->state_ = STATE_CHECK_AT;
+        } else {
+          // Keep waiting registration
+          this->state_ = STATE_CREG;
+        }
+      }
+      this->registered_ = registered;
+      break;
+    }
+    case STATE_CSQ:
+      send_cmd_("AT+CSQ");
+      this->state_ = STATE_CSQ_RESPONSE;
+      break;
+    case STATE_CSQ_RESPONSE:
+      if (message.compare(0, 5, "+CSQ:") == 0) {
+        size_t comma = message.find(',', 6);
+        if (comma != 6) {
+          this->rssi_ = strtol(message.substr(6, comma - 6).c_str(), nullptr, 10);
+          ESP_LOGD(TAG, "RSSI: %d", this->rssi_);
+        }
+      }
+      this->expect_ack_ = true;
+      this->state_ = STATE_CHECK_SMS;
+      break;
+    case STATE_PARSE_SMS:
+      this->state_ = STATE_PARSE_SMS_RESPONSE;
+      break;
+    case STATE_PARSE_SMS_RESPONSE:
+      if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) {
+        size_t start = 7;
+        size_t end = message.find(',', start);
+        uint8_t item = 0;
+        while (end != start) {
+          item++;
+          if (item == 1) {  // Slot Index
+            this->parse_index_ = strtol(message.substr(start, end - start).c_str(), nullptr, 10);
+          }
+          // item 2 = STATUS, usually "REC UNERAD"
+          if (item == 3) {  // recipient
+            // Add 1 and remove 2 from substring to get rid of "quotes"
+            this->sender_ = message.substr(start + 1, end - start - 2);
+            break;
+          }
+          // item 4 = ""
+          // item 5 = Received timestamp
+          start = end + 1;
+          end = message.find(',', start);
+        }
+
+        if (item < 2) {
+          ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str());
+          return;
+        }
+        this->state_ = STATE_RECEIVESMS;
+      }
+      // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS
+      if (message == "OK")
+        this->state_ = STATE_INIT;
+      break;
+    case STATE_RECEIVESMS:
+      /* Our recipient is set and the message body is in message
+        kick ESPHome callback now
+      */
+      ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str());
+      ESP_LOGD(TAG, "%s", message.c_str());
+      this->callback_.call(message, this->sender_);
+      /* If the message is multiline, next lines will contain message data.
+         If there were other messages in the list, next line will be +CMGL: ...
+         At the end of the list the new line and the OK should be received.
+         To keep this simple just first line of message if considered, then
+         the next state will swallow all received data and in next poll event
+         this message index is marked for deletion.
+      */
+      this->state_ = STATE_RECEIVEDSMS;
+      break;
+    case STATE_RECEIVEDSMS:
+      // Let the buffer flush. Next poll will request to delete the parsed index message.
+      break;
+    case STATE_SENDINGSMS1:
+      this->send_cmd_("AT+CMGS=\"" + this->recipient_ + "\"");
+      this->state_ = STATE_SENDINGSMS2;
+      break;
+    case STATE_SENDINGSMS2:
+      if (message == ">") {
+        // Send sms body
+        ESP_LOGD(TAG, "Sending message: '%s'", this->outgoing_message_.c_str());
+        this->write_str(this->outgoing_message_.c_str());
+        this->write(26);
+        this->state_ = STATE_SENDINGSMS3;
+      } else {
+        this->registered_ = false;
+        this->state_ = STATE_INIT;
+        this->send_cmd_("AT+CMEE=2");
+        this->write(26);
+      }
+      break;
+    case STATE_SENDINGSMS3:
+      if (message.compare(0, 6, "+CMGS:") == 0) {
+        ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str());
+        this->send_pending_ = false;
+        this->state_ = STATE_CHECK_SMS;
+        this->expect_ack_ = true;
+      }
+      break;
+    default:
+      ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_);
+      break;
+  }
+}
+
+void Sim800LComponent::loop() {
+  // Read message
+  while (this->available()) {
+    uint8_t byte;
+    this->read_byte(&byte);
+
+    if (this->read_pos_ == SIM800L_READ_BUFFER_LENGTH)
+      this->read_pos_ = 0;
+
+    ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);  // NOLINT
+
+    if (byte == ASCII_CR)
+      continue;
+    if (byte >= 0x7F)
+      byte = '?';  // need to be valid utf8 string for log functions.
+    this->read_buffer_[this->read_pos_] = byte;
+
+    if (this->state_ == STATE_SENDINGSMS2 && this->read_pos_ == 0 && byte == '>')
+      this->read_buffer_[++this->read_pos_] = ASCII_LF;
+
+    if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
+      this->read_buffer_[this->read_pos_] = 0;
+      this->read_pos_ = 0;
+      this->parse_cmd_(this->read_buffer_);
+    } else {
+      this->read_pos_++;
+    }
+  }
+}
+
+void Sim800LComponent::send_sms(std::string recipient, std::string message) {
+  ESP_LOGD(TAG, "Sending to %s: %s", recipient.c_str(), message.c_str());
+  this->recipient_ = recipient;
+  this->outgoing_message_ = message;
+  this->send_pending_ = true;
+  this->update();
+}
+void Sim800LComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "SIM800L:");
+  ESP_LOGCONFIG(TAG, "  RSSI: %d dB", this->rssi_);
+}
+
+}  // namespace sim800l
+}  // namespace esphome
diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h
new file mode 100644
index 0000000000..696eb8890f
--- /dev/null
+++ b/esphome/components/sim800l/sim800l.h
@@ -0,0 +1,92 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/uart/uart.h"
+#include "esphome/core/automation.h"
+
+namespace esphome {
+namespace sim800l {
+
+const uint8_t SIM800L_READ_BUFFER_LENGTH = 255;
+
+enum State {
+  STATE_IDLE = 0,
+  STATE_INIT,
+  STATE_CHECK_AT,
+  STATE_CREG,
+  STATE_CREGWAIT,
+  STATE_CSQ,
+  STATE_CSQ_RESPONSE,
+  STATE_IDLEWAIT,
+  STATE_SENDINGSMS1,
+  STATE_SENDINGSMS2,
+  STATE_SENDINGSMS3,
+  STATE_CHECK_SMS,
+  STATE_PARSE_SMS,
+  STATE_PARSE_SMS_RESPONSE,
+  STATE_RECEIVESMS,
+  STATE_READSMS,
+  STATE_RECEIVEDSMS,
+  STATE_DELETEDSMS,
+  STATE_DISABLE_ECHO,
+  STATE_PARSE_SMS_OK
+};
+
+class Sim800LComponent : public uart::UARTDevice, public PollingComponent {
+ public:
+  /// Retrieve the latest sensor values. This operation takes approximately 16ms.
+  void update() override;
+  void loop() override;
+  void dump_config() override;
+  void add_on_sms_received_callback(std::function<void(std::string, std::string)> callback) {
+    this->callback_.add(std::move(callback));
+  }
+  void send_sms(std::string recipient, std::string message);
+
+ protected:
+  void send_cmd_(std::string);
+  void parse_cmd_(std::string);
+
+  std::string sender_;
+  char read_buffer_[SIM800L_READ_BUFFER_LENGTH];
+  size_t read_pos_{0};
+  uint8_t parse_index_{0};
+  uint8_t watch_dog_{0};
+  bool expect_ack_{false};
+  sim800l::State state_{STATE_IDLE};
+  bool registered_{false};
+  int rssi_{0};
+
+  std::string recipient_;
+  std::string outgoing_message_;
+  bool send_pending_;
+
+  CallbackManager<void(std::string, std::string)> callback_;
+};
+
+class Sim800LReceivedMessageTrigger : public Trigger<std::string, std::string> {
+ public:
+  explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) {
+    parent->add_on_sms_received_callback(
+        [this](std::string message, std::string sender) { this->trigger(message, sender); });
+  }
+};
+
+template<typename... Ts> class Sim800LSendSmsAction : public Action<Ts...> {
+ public:
+  Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {}
+  TEMPLATABLE_VALUE(std::string, recipient)
+  TEMPLATABLE_VALUE(std::string, message)
+
+  void play(Ts... x) {
+    auto recipient = this->recipient_.value(x...);
+    auto message = this->message_.value(x...);
+    this->parent_->send_sms(recipient, message);
+  }
+
+ protected:
+  Sim800LComponent *parent_;
+};
+
+}  // namespace sim800l
+}  // namespace esphome
diff --git a/esphome/components/sm16716/__init__.py b/esphome/components/sm16716/__init__.py
new file mode 100644
index 0000000000..4e342588f9
--- /dev/null
+++ b/esphome/components/sm16716/__init__.py
@@ -0,0 +1,31 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import (CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID,
+                           CONF_NUM_CHANNELS, CONF_NUM_CHIPS)
+
+AUTO_LOAD = ['output']
+sm16716_ns = cg.esphome_ns.namespace('sm16716')
+SM16716 = sm16716_ns.class_('SM16716', cg.Component)
+
+MULTI_CONF = True
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SM16716),
+    cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema,
+    cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema,
+    cv.Optional(CONF_NUM_CHANNELS, default=3): cv.int_range(min=3, max=255),
+    cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+
+    data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    cg.add(var.set_data_pin(data))
+    clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    cg.add(var.set_clock_pin(clock))
+
+    cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS]))
+    cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
diff --git a/esphome/components/sm16716/output.py b/esphome/components/sm16716/output.py
new file mode 100644
index 0000000000..93c9ed4ce1
--- /dev/null
+++ b/esphome/components/sm16716/output.py
@@ -0,0 +1,25 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID
+from . import SM16716
+
+DEPENDENCIES = ['sm16716']
+
+Channel = SM16716.class_('Channel', output.FloatOutput)
+
+CONF_SM16716_ID = 'sm16716_id'
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
+    cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716),
+    cv.Required(CONF_ID): cv.declare_id(Channel),
+    cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield output.register_output(var, config)
+
+    parent = yield cg.get_variable(config[CONF_SM16716_ID])
+    cg.add(var.set_parent(parent))
+    cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp
new file mode 100644
index 0000000000..bc8e4fc1f4
--- /dev/null
+++ b/esphome/components/sm16716/sm16716.cpp
@@ -0,0 +1,52 @@
+#include "sm16716.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sm16716 {
+
+static const char *TAG = "sm16716";
+
+void SM16716::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up SM16716OutputComponent...");
+  this->data_pin_->setup();
+  this->data_pin_->digital_write(false);
+  this->clock_pin_->setup();
+  this->clock_pin_->digital_write(false);
+  this->pwm_amounts_.resize(this->num_channels_, 0);
+}
+void SM16716::dump_config() {
+  ESP_LOGCONFIG(TAG, "SM16716:");
+  LOG_PIN("  Data Pin: ", this->data_pin_);
+  LOG_PIN("  Clock Pin: ", this->clock_pin_);
+  ESP_LOGCONFIG(TAG, "  Total number of channels: %u", this->num_channels_);
+  ESP_LOGCONFIG(TAG, "  Number of chips: %u", this->num_chips_);
+}
+void SM16716::loop() {
+  if (!this->update_)
+    return;
+
+  for (uint8_t i = 0; i < 50; i++) {
+    this->write_bit_(false);
+  }
+
+  // send 25 bits (1 start bit plus 24 data bits) for each chip
+  for (uint8_t index = 0; index < this->num_channels_; index++) {
+    // send a start bit initially and after every 3 channels
+    if (index % 3 == 0) {
+      this->write_bit_(true);
+    }
+
+    this->write_byte_(this->pwm_amounts_[index]);
+  }
+
+  // send a blank 25 bits to signal the end
+  this->write_bit_(false);
+  this->write_byte_(0);
+  this->write_byte_(0);
+  this->write_byte_(0);
+
+  this->update_ = false;
+}
+
+}  // namespace sm16716
+}  // namespace esphome
diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h
new file mode 100644
index 0000000000..85f78c8cf5
--- /dev/null
+++ b/esphome/components/sm16716/sm16716.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/esphal.h"
+#include "esphome/components/output/float_output.h"
+
+namespace esphome {
+namespace sm16716 {
+
+class SM16716 : public Component {
+ public:
+  class Channel;
+
+  void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
+  void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
+  void set_num_channels(uint8_t num_channels) { num_channels_ = num_channels; }
+  void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; }
+
+  void setup() override;
+
+  void dump_config() override;
+
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  /// Send new values if they were updated.
+  void loop() override;
+
+  class Channel : public output::FloatOutput {
+   public:
+    void set_parent(SM16716 *parent) { parent_ = parent; }
+    void set_channel(uint8_t channel) { channel_ = channel; }
+
+   protected:
+    void write_state(float state) override {
+      auto amount = uint8_t(state * 0xFF);
+      this->parent_->set_channel_value_(this->channel_, amount);
+    }
+
+    SM16716 *parent_;
+    uint8_t channel_;
+  };
+
+ protected:
+  void set_channel_value_(uint8_t channel, uint8_t value) {
+    uint8_t index = this->num_channels_ - channel - 1;
+    if (this->pwm_amounts_[index] != value) {
+      this->update_ = true;
+    }
+    this->pwm_amounts_[index] = value;
+  }
+  void write_bit_(bool value) {
+    this->data_pin_->digital_write(value);
+    this->clock_pin_->digital_write(true);
+    this->clock_pin_->digital_write(false);
+  }
+  void write_byte_(uint8_t data) {
+    for (uint8_t mask = 0x80; mask; mask >>= 1) {
+      this->write_bit_(data & mask);
+    }
+  }
+
+  GPIOPin *data_pin_;
+  GPIOPin *clock_pin_;
+  uint8_t num_channels_;
+  uint8_t num_chips_;
+  std::vector<uint8_t> pwm_amounts_;
+  bool update_{true};
+};
+
+}  // namespace sm16716
+}  // namespace esphome
diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp
index 4f931203fb..c10a3e5ac3 100644
--- a/esphome/components/sntp/sntp_component.cpp
+++ b/esphome/components/sntp/sntp_component.cpp
@@ -2,7 +2,7 @@
 #include "esphome/core/log.h"
 
 #ifdef ARDUINO_ARCH_ESP32
-#include "apps/sntp/sntp.h"
+#include "lwip/apps/sntp.h"
 #endif
 #ifdef ARDUINO_ARCH_ESP8266
 #include "sntp.h"
diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py
index ae6d09dfac..65ee5960f0 100644
--- a/esphome/components/speed/fan/__init__.py
+++ b/esphome/components/speed/fan/__init__.py
@@ -29,4 +29,4 @@ def to_code(config):
 
     if CONF_OSCILLATION_OUTPUT in config:
         oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
-        cg.add(var.set_oscillation(oscillation_output))
+        cg.add(var.set_oscillating(oscillation_output))
diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp
index db4b71c29a..bf2a18955a 100644
--- a/esphome/components/spi/spi.cpp
+++ b/esphome/components/spi/spi.cpp
@@ -8,74 +8,10 @@ namespace spi {
 
 static const char *TAG = "spi";
 
-void ICACHE_RAM_ATTR HOT SPIComponent::write_byte(uint8_t data) {
-  uint8_t send_bits = data;
-  if (this->msb_first_)
-    send_bits = reverse_bits_8(data);
-
-  this->clk_->digital_write(true);
-  if (!this->high_speed_)
-    delayMicroseconds(5);
-
-  for (size_t i = 0; i < 8; i++) {
-    if (!this->high_speed_)
-      delayMicroseconds(5);
-    this->clk_->digital_write(false);
-
-    // sampling on leading edge
-    this->mosi_->digital_write(send_bits & (1 << i));
-    if (!this->high_speed_)
-      delayMicroseconds(5);
-    this->clk_->digital_write(true);
-  }
-
-  ESP_LOGVV(TAG, "    Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data);
-}
-
-uint8_t ICACHE_RAM_ATTR HOT SPIComponent::read_byte() {
-  this->clk_->digital_write(true);
-
-  uint8_t data = 0;
-  for (size_t i = 0; i < 8; i++) {
-    if (!this->high_speed_)
-      delayMicroseconds(5);
-    data |= uint8_t(this->miso_->digital_read()) << i;
-    this->clk_->digital_write(false);
-    if (!this->high_speed_)
-      delayMicroseconds(5);
-    this->clk_->digital_write(true);
-  }
-
-  if (this->msb_first_) {
-    data = reverse_bits_8(data);
-  }
-
-  ESP_LOGVV(TAG, "    Received 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data);
-
-  return data;
-}
-void ICACHE_RAM_ATTR HOT SPIComponent::read_array(uint8_t *data, size_t length) {
-  for (size_t i = 0; i < length; i++)
-    data[i] = this->read_byte();
-}
-
-void ICACHE_RAM_ATTR HOT SPIComponent::write_array(uint8_t *data, size_t length) {
-  for (size_t i = 0; i < length; i++) {
-    App.feed_wdt();
-    this->write_byte(data[i]);
-  }
-}
-
-void ICACHE_RAM_ATTR HOT SPIComponent::enable(GPIOPin *cs, bool msb_first, bool high_speed) {
-  ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", cs->get_pin());
-  cs->digital_write(false);
-
-  this->active_cs_ = cs;
-  this->msb_first_ = msb_first;
-  this->high_speed_ = high_speed;
-}
-
 void ICACHE_RAM_ATTR HOT SPIComponent::disable() {
+  if (this->hw_spi_ != nullptr) {
+    this->hw_spi_->endTransaction();
+  }
   ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin());
   this->active_cs_->digital_write(true);
   this->active_cs_ = nullptr;
@@ -84,6 +20,53 @@ void SPIComponent::setup() {
   ESP_LOGCONFIG(TAG, "Setting up SPI bus...");
   this->clk_->setup();
   this->clk_->digital_write(true);
+
+  bool use_hw_spi = true;
+  if (this->clk_->is_inverted())
+    use_hw_spi = false;
+  const bool has_miso = this->miso_ != nullptr;
+  const bool has_mosi = this->mosi_ != nullptr;
+  if (has_miso && this->miso_->is_inverted())
+    use_hw_spi = false;
+  if (has_mosi && this->mosi_->is_inverted())
+    use_hw_spi = false;
+  int8_t clk_pin = this->clk_->get_pin();
+  int8_t miso_pin = has_miso ? this->miso_->get_pin() : -1;
+  int8_t mosi_pin = has_mosi ? this->mosi_->get_pin() : -1;
+#ifdef ARDUINO_ARCH_ESP8266
+  if (clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) {
+    // pass
+  } else if (clk_pin == 14 && miso_pin == 12 && mosi_pin == 13) {
+    // pass
+  } else {
+    use_hw_spi = false;
+  }
+
+  if (use_hw_spi) {
+    this->hw_spi_ = &SPI;
+    this->hw_spi_->pins(clk_pin, miso_pin, mosi_pin, 0);
+    this->hw_spi_->begin();
+    return;
+  }
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  static uint8_t spi_bus_num = 0;
+  if (spi_bus_num >= 2) {
+    use_hw_spi = false;
+  }
+
+  if (use_hw_spi) {
+    if (spi_bus_num == 0) {
+      this->hw_spi_ = &SPI;
+    } else {
+      this->hw_spi_ = new SPIClass(VSPI);
+    }
+    spi_bus_num++;
+    this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin);
+    return;
+  }
+#endif
+
   if (this->miso_ != nullptr) {
     this->miso_->setup();
   }
@@ -97,8 +80,154 @@ void SPIComponent::dump_config() {
   LOG_PIN("  CLK Pin: ", this->clk_);
   LOG_PIN("  MISO Pin: ", this->miso_);
   LOG_PIN("  MOSI Pin: ", this->mosi_);
+  ESP_LOGCONFIG(TAG, "  Using HW SPI: %s", YESNO(this->hw_spi_ != nullptr));
 }
 float SPIComponent::get_setup_priority() const { return setup_priority::BUS; }
 
+void SPIComponent::debug_tx(uint8_t value) {
+  ESP_LOGVV(TAG, "    TX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value);
+}
+void SPIComponent::debug_rx(uint8_t value) {
+  ESP_LOGVV(TAG, "    RX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value);
+}
+void SPIComponent::debug_enable(uint8_t pin) { ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", pin); }
+
+void SPIComponent::cycle_clock_(bool value) {
+  uint32_t start = ESP.getCycleCount();
+  while (start - ESP.getCycleCount() < this->wait_cycle_)
+    ;
+  this->clk_->digital_write(value);
+  start += this->wait_cycle_;
+  while (start - ESP.getCycleCount() < this->wait_cycle_)
+    ;
+}
+
+// NOLINTNEXTLINE
+#pragma GCC optimize("unroll-loops")
+// NOLINTNEXTLINE
+#pragma GCC optimize("O2")
+
+template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE>
+uint8_t HOT SPIComponent::transfer_(uint8_t data) {
+  // Clock starts out at idle level
+  this->clk_->digital_write(CLOCK_POLARITY);
+  uint8_t out_data = 0;
+
+  for (uint8_t i = 0; i < 8; i++) {
+    uint8_t shift;
+    if (BIT_ORDER == BIT_ORDER_MSB_FIRST)
+      shift = 7 - i;
+    else
+      shift = i;
+
+    if (CLOCK_PHASE == CLOCK_PHASE_LEADING) {
+      // sampling on leading edge
+      if (WRITE) {
+        this->mosi_->digital_write(data & (1 << shift));
+      }
+
+      // SAMPLE!
+      this->cycle_clock_(!CLOCK_POLARITY);
+
+      if (READ) {
+        out_data |= uint8_t(this->miso_->digital_read()) << shift;
+      }
+
+      this->cycle_clock_(CLOCK_POLARITY);
+    } else {
+      // sampling on trailing edge
+      this->cycle_clock_(!CLOCK_POLARITY);
+
+      if (WRITE) {
+        this->mosi_->digital_write(data & (1 << shift));
+      }
+
+      // SAMPLE!
+      this->cycle_clock_(CLOCK_POLARITY);
+
+      if (READ) {
+        out_data |= uint8_t(this->miso_->digital_read()) << shift;
+      }
+    }
+  }
+
+#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
+  if (WRITE) {
+    SPIComponent::debug_tx(data);
+  }
+  if (READ) {
+    SPIComponent::debug_rx(out_data);
+  }
+#endif
+
+  App.feed_wdt();
+
+  return out_data;
+}
+
+// Generate with (py3):
+//
+// from itertools import product
+// bit_orders = ['BIT_ORDER_LSB_FIRST', 'BIT_ORDER_MSB_FIRST']
+// clock_pols = ['CLOCK_POLARITY_LOW', 'CLOCK_POLARITY_HIGH']
+// clock_phases = ['CLOCK_PHASE_LEADING', 'CLOCK_PHASE_TRAILING']
+// reads = [False, True]
+// writes = [False, True]
+// cpp_bool = {False: 'false', True: 'true'}
+// for b, cpol, cph, r, w in product(bit_orders, clock_pols, clock_phases, reads, writes):
+//     if not r and not w:
+//         continue
+//     print(f"template uint8_t SPIComponent::transfer_<{b}, {cpol}, {cph}, {cpp_bool[r]}, {cpp_bool[w]}>(uint8_t
+//     data);")
+
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>(
+    uint8_t data);
+template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>(
+    uint8_t data);
+
 }  // namespace spi
 }  // namespace esphome
diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h
index 600e1a0cd2..ccef6192f3 100644
--- a/esphome/components/spi/spi.h
+++ b/esphome/components/spi/spi.h
@@ -2,10 +2,61 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/esphal.h"
+#include <SPI.h>
 
 namespace esphome {
 namespace spi {
 
+/// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted.
+enum SPIBitOrder {
+  /// The least significant bit is transmitted/received first.
+  BIT_ORDER_LSB_FIRST,
+  /// The most significant bit is transmitted/received first.
+  BIT_ORDER_MSB_FIRST,
+};
+/** The SPI clock signal polarity,
+ *
+ * This defines how the clock signal is used. Flipping this effectively inverts the clock signal.
+ */
+enum SPIClockPolarity {
+  /** The clock signal idles on LOW. (CPOL=0)
+   *
+   * A rising edge means a leading edge for the clock.
+   */
+  CLOCK_POLARITY_LOW = false,
+  /** The clock signal idles on HIGH. (CPOL=1)
+   *
+   * A falling edge means a trailing edge for the clock.
+   */
+  CLOCK_POLARITY_HIGH = true,
+};
+/** The SPI clock signal phase.
+ *
+ * This defines when the data signals are sampled. Most SPI devices use the LEADING clock phase.
+ */
+enum SPIClockPhase {
+  /// The data is sampled on a leading clock edge. (CPHA=0)
+  CLOCK_PHASE_LEADING,
+  /// The data is sampled on a trailing clock edge. (CPHA=1)
+  CLOCK_PHASE_TRAILING,
+};
+/** The SPI clock signal data rate. This defines for what duration the clock signal is HIGH/LOW.
+ * So effectively the rate of bytes can be calculated using
+ *
+ * effective_byte_rate = spi_data_rate / 16
+ *
+ * Implementations can use the pre-defined constants here, or use an integer in the template definition
+ * to manually use a specific data rate.
+ */
+enum SPIDataRate : uint32_t {
+  DATA_RATE_1KHZ = 1000,
+  DATA_RATE_200KHZ = 200000,
+  DATA_RATE_1MHZ = 1000000,
+  DATA_RATE_2MHZ = 2000000,
+  DATA_RATE_4MHZ = 4000000,
+  DATA_RATE_8MHZ = 8000000,
+};
+
 class SPIComponent : public Component {
  public:
   void set_clk(GPIOPin *clk) { clk_ = clk; }
@@ -16,59 +67,156 @@ class SPIComponent : public Component {
 
   void dump_config() override;
 
-  uint8_t read_byte();
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> uint8_t read_byte() {
+    if (this->hw_spi_ != nullptr) {
+      return this->hw_spi_->transfer(0x00);
+    }
+    return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, false>(0x00);
+  }
 
-  void read_array(uint8_t *data, size_t length);
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
+  void read_array(uint8_t *data, size_t length) {
+    if (this->hw_spi_ != nullptr) {
+      this->hw_spi_->transfer(data, length);
+      return;
+    }
+    for (size_t i = 0; i < length; i++) {
+      data[i] = this->read_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>();
+    }
+  }
 
-  void write_byte(uint8_t data);
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
+  void write_byte(uint8_t data) {
+    if (this->hw_spi_ != nullptr) {
+      this->hw_spi_->write(data);
+      return;
+    }
+    this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, false, true>(data);
+  }
 
-  void write_array(uint8_t *data, size_t length);
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
+  void write_array(const uint8_t *data, size_t length) {
+    if (this->hw_spi_ != nullptr) {
+      auto *data_c = const_cast<uint8_t *>(data);
+      this->hw_spi_->writeBytes(data_c, length);
+      return;
+    }
+    for (size_t i = 0; i < length; i++) {
+      this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
+    }
+  }
 
-  void enable(GPIOPin *cs, bool msb_first, bool high_speed);
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
+  uint8_t transfer_byte(uint8_t data) {
+    if (this->hw_spi_ != nullptr) {
+      return this->hw_spi_->transfer(data);
+    }
+    return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data);
+  }
+
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE>
+  void transfer_array(uint8_t *data, size_t length) {
+    if (this->hw_spi_ != nullptr) {
+      this->hw_spi_->transfer(data, length);
+      return;
+    }
+    for (size_t i = 0; i < length; i++) {
+      data[i] = this->transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]);
+    }
+  }
+
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, uint32_t DATA_RATE>
+  void enable(GPIOPin *cs) {
+    SPIComponent::debug_enable(cs->get_pin());
+
+    if (this->hw_spi_ != nullptr) {
+      uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE);
+      SPISettings settings(DATA_RATE, BIT_ORDER, data_mode);
+      this->hw_spi_->beginTransaction(settings);
+    } else {
+      this->clk_->digital_write(CLOCK_POLARITY);
+      this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL;
+    }
+
+    this->active_cs_ = cs;
+    this->active_cs_->digital_write(false);
+  }
 
   void disable();
 
   float get_setup_priority() const override;
 
  protected:
+  inline void cycle_clock_(bool value);
+
+  static void debug_enable(uint8_t pin);
+  static void debug_tx(uint8_t value);
+  static void debug_rx(uint8_t value);
+
+  template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE>
+  uint8_t transfer_(uint8_t data);
+
   GPIOPin *clk_;
   GPIOPin *miso_{nullptr};
   GPIOPin *mosi_{nullptr};
   GPIOPin *active_cs_{nullptr};
-  bool msb_first_{true};
-  bool high_speed_{false};
+  SPIClass *hw_spi_{nullptr};
+  uint32_t wait_cycle_;
 };
 
+template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, SPIDataRate DATA_RATE>
 class SPIDevice {
  public:
   SPIDevice() = default;
   SPIDevice(SPIComponent *parent, GPIOPin *cs) : parent_(parent), cs_(cs) {}
 
-  void set_spi_parent(SPIComponent *parent) { this->parent_ = parent; }
-  void set_cs_pin(GPIOPin *cs) { this->cs_ = cs; }
+  void set_spi_parent(SPIComponent *parent) { parent_ = parent; }
+  void set_cs_pin(GPIOPin *cs) { cs_ = cs; }
 
   void spi_setup() {
     this->cs_->setup();
     this->cs_->digital_write(true);
   }
 
-  void enable() { this->parent_->enable(this->cs_, this->is_device_msb_first(), this->is_device_high_speed()); }
+  void enable() { this->parent_->template enable<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, DATA_RATE>(this->cs_); }
 
   void disable() { this->parent_->disable(); }
 
-  uint8_t read_byte() { return this->parent_->read_byte(); }
+  uint8_t read_byte() { return this->parent_->template read_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(); }
 
-  void read_array(uint8_t *data, size_t length) { return this->parent_->read_array(data, length); }
+  void read_array(uint8_t *data, size_t length) {
+    return this->parent_->template read_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
+  }
 
-  void write_byte(uint8_t data) { return this->parent_->write_byte(data); }
+  template<size_t N> std::array<uint8_t, N> read_array() {
+    std::array<uint8_t, N> data;
+    this->read_array(data.data(), N);
+    return data;
+  }
 
-  void write_array(uint8_t *data, size_t length) { this->parent_->write_array(data, length); }
+  void write_byte(uint8_t data) {
+    return this->parent_->template write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
+  }
+
+  void write_array(const uint8_t *data, size_t length) {
+    this->parent_->template write_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
+  }
+
+  template<size_t N> void write_array(const std::array<uint8_t, N> &data) { this->write_array(data.data(), N); }
+
+  void write_array(const std::vector<uint8_t> &data) { this->write_array(data.data(), data.size()); }
+
+  uint8_t transfer_byte(uint8_t data) {
+    return this->parent_->template transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data);
+  }
+
+  void transfer_array(uint8_t *data, size_t length) {
+    this->parent_->template transfer_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length);
+  }
+
+  template<size_t N> void transfer_array(std::array<uint8_t, N> &data) { this->transfer_array(data.data(), N); }
 
  protected:
-  virtual bool is_device_msb_first() = 0;
-
-  virtual bool is_device_high_speed() { return false; }
-
   SPIComponent *parent_{nullptr};
   GPIOPin *cs_{nullptr};
 };
diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py
index 0a678452b2..047ddddcac 100644
--- a/esphome/components/ssd1306_base/__init__.py
+++ b/esphome/components/ssd1306_base/__init__.py
@@ -2,7 +2,8 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import display
-from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN
+from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN, \
+    CONF_BRIGHTNESS
 from esphome.core import coroutine
 
 ssd1306_base_ns = cg.esphome_ns.namespace('ssd1306_base')
@@ -25,6 +26,7 @@ SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_")
 SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({
     cv.Required(CONF_MODEL): SSD1306_MODEL,
     cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
+    cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
     cv.Optional(CONF_EXTERNAL_VCC): cv.boolean,
 }).extend(cv.polling_component_schema('1s'))
 
@@ -38,6 +40,8 @@ def setup_ssd1036(var, config):
     if CONF_RESET_PIN in config:
         reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
+    if CONF_BRIGHTNESS in config:
+        cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
     if CONF_EXTERNAL_VCC in config:
         cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC]))
     if CONF_LAMBDA in config:
diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp
index b6f2d94eac..d60f7dc985 100644
--- a/esphome/components/ssd1306_base/ssd1306_base.cpp
+++ b/esphome/components/ssd1306_base/ssd1306_base.cpp
@@ -79,10 +79,7 @@ void SSD1306::setup() {
     case SH1106_MODEL_128_64:
     case SSD1306_MODEL_64_48:
     case SH1106_MODEL_64_48:
-      if (this->external_vcc_)
-        this->command(0x9F);
-      else
-        this->command(0xCF);
+      this->command(int(255 * (this->brightness_)));
       break;
     case SSD1306_MODEL_96_16:
     case SH1106_MODEL_96_16:
@@ -100,7 +97,7 @@ void SSD1306::setup() {
     this->command(0xF1);
 
   this->command(SSD1306_COMMAND_SET_VCOM_DETECT);
-  this->command(0x40);
+  this->command(0x00);
 
   this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME);
   this->command(SSD1306_NORMAL_DISPLAY);
diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h
index 66c12ec938..8adf3c1b87 100644
--- a/esphome/components/ssd1306_base/ssd1306_base.h
+++ b/esphome/components/ssd1306_base/ssd1306_base.h
@@ -29,6 +29,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
   void set_model(SSD1306Model model) { this->model_ = model; }
   void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
   void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; }
+  void set_brightness(float brightness) { this->brightness_ = brightness; }
 
   float get_setup_priority() const override { return setup_priority::PROCESSOR; }
   void fill(int color) override;
@@ -50,6 +51,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer {
   SSD1306Model model_{SSD1306_MODEL_128_64};
   GPIOPin *reset_pin_{nullptr};
   bool external_vcc_{false};
+  float brightness_{1.0};
 };
 
 }  // namespace ssd1306_base
diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp
index aeead612ff..d87f412f70 100644
--- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp
+++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp
@@ -7,7 +7,6 @@ namespace ssd1306_spi {
 
 static const char *TAG = "ssd1306_spi";
 
-bool SPISSD1306::is_device_msb_first() { return true; }
 void SPISSD1306::setup() {
   ESP_LOGCONFIG(TAG, "Setting up SPI SSD1306...");
   this->spi_setup();
@@ -52,7 +51,6 @@ void HOT SPISSD1306::write_display_data() {
     this->disable();
   }
 }
-bool SPISSD1306::is_device_high_speed() { return true; }
 
 }  // namespace ssd1306_spi
 }  // namespace esphome
diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.h b/esphome/components/ssd1306_spi/ssd1306_spi.h
index 5d0640bd84..c58ebc800a 100644
--- a/esphome/components/ssd1306_spi/ssd1306_spi.h
+++ b/esphome/components/ssd1306_spi/ssd1306_spi.h
@@ -7,7 +7,9 @@
 namespace esphome {
 namespace ssd1306_spi {
 
-class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice {
+class SPISSD1306 : public ssd1306_base::SSD1306,
+                   public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
+                                         spi::DATA_RATE_8MHZ> {
  public:
   void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
 
@@ -19,8 +21,6 @@ class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice {
   void command(uint8_t value) override;
 
   void write_display_data() override;
-  bool is_device_msb_first() override;
-  bool is_device_high_speed() override;
 
   GPIOPin *dc_pin_;
 };
diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py
new file mode 100644
index 0000000000..69e11ec0d1
--- /dev/null
+++ b/esphome/components/ssd1325_base/__init__.py
@@ -0,0 +1,42 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import display
+from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN
+from esphome.core import coroutine
+
+ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base')
+SSD1325 = ssd1325_base_ns.class_('SSD1325', cg.PollingComponent, display.DisplayBuffer)
+SSD1325Model = ssd1325_base_ns.enum('SSD1325Model')
+
+MODELS = {
+    'SSD1325_128X32': SSD1325Model.SSD1325_MODEL_128_32,
+    'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64,
+    'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16,
+    'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48,
+}
+
+SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_")
+
+SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({
+    cv.Required(CONF_MODEL): SSD1325_MODEL,
+    cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
+    cv.Optional(CONF_EXTERNAL_VCC): cv.boolean,
+}).extend(cv.polling_component_schema('1s'))
+
+
+@coroutine
+def setup_ssd1036(var, config):
+    yield cg.register_component(var, config)
+    yield display.register_display(var, config)
+
+    cg.add(var.set_model(config[CONF_MODEL]))
+    if CONF_RESET_PIN in config:
+        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        cg.add(var.set_reset_pin(reset))
+    if CONF_EXTERNAL_VCC in config:
+        cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC]))
+    if CONF_LAMBDA in config:
+        lambda_ = yield cg.process_lambda(
+            config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void)
+        cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp
new file mode 100644
index 0000000000..3079e19cc8
--- /dev/null
+++ b/esphome/components/ssd1325_base/ssd1325_base.cpp
@@ -0,0 +1,177 @@
+#include "ssd1325_base.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace ssd1325_base {
+
+static const char *TAG = "ssd1325";
+
+static const uint8_t BLACK = 0;
+static const uint8_t WHITE = 1;
+
+static const uint8_t SSD1325_SETCOLADDR = 0x15;
+static const uint8_t SSD1325_SETROWADDR = 0x75;
+static const uint8_t SSD1325_SETCONTRAST = 0x81;
+static const uint8_t SSD1325_SETCURRENT = 0x84;
+
+static const uint8_t SSD1325_SETREMAP = 0xA0;
+static const uint8_t SSD1325_SETSTARTLINE = 0xA1;
+static const uint8_t SSD1325_SETOFFSET = 0xA2;
+static const uint8_t SSD1325_NORMALDISPLAY = 0xA4;
+static const uint8_t SSD1325_DISPLAYALLON = 0xA5;
+static const uint8_t SSD1325_DISPLAYALLOFF = 0xA6;
+static const uint8_t SSD1325_INVERTDISPLAY = 0xA7;
+static const uint8_t SSD1325_SETMULTIPLEX = 0xA8;
+static const uint8_t SSD1325_MASTERCONFIG = 0xAD;
+static const uint8_t SSD1325_DISPLAYOFF = 0xAE;
+static const uint8_t SSD1325_DISPLAYON = 0xAF;
+
+static const uint8_t SSD1325_SETPRECHARGECOMPENABLE = 0xB0;
+static const uint8_t SSD1325_SETPHASELEN = 0xB1;
+static const uint8_t SSD1325_SETROWPERIOD = 0xB2;
+static const uint8_t SSD1325_SETCLOCK = 0xB3;
+static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4;
+static const uint8_t SSD1325_SETGRAYTABLE = 0xB8;
+static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC;
+static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE;
+static const uint8_t SSD1325_SETVSL = 0xBF;
+
+static const uint8_t SSD1325_GFXACCEL = 0x23;
+static const uint8_t SSD1325_DRAWRECT = 0x24;
+static const uint8_t SSD1325_COPY = 0x25;
+
+void SSD1325::setup() {
+  this->init_internal_(this->get_buffer_length_());
+
+  this->command(SSD1325_DISPLAYOFF);   /* display off */
+  this->command(SSD1325_SETCLOCK);     /* set osc division */
+  this->command(0xF1);                 /* 145 */
+  this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */
+  this->command(0x3f);                 /* duty = 1/64 */
+  this->command(SSD1325_SETOFFSET);    /* set display offset --- */
+  this->command(0x4C);                 /* 76 */
+  this->command(SSD1325_SETSTARTLINE); /*set start line */
+  this->command(0x00);                 /* ------ */
+  this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/
+  this->command(0x02);
+  this->command(SSD1325_SETREMAP); /* set segment remap------ */
+  this->command(0x56);
+  this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */
+  this->command(SSD1325_SETGRAYTABLE);
+  this->command(0x01);
+  this->command(0x11);
+  this->command(0x22);
+  this->command(0x32);
+  this->command(0x43);
+  this->command(0x54);
+  this->command(0x65);
+  this->command(0x76);
+  this->command(SSD1325_SETCONTRAST); /* set contrast current */
+  this->command(0x7F);                // max!
+  this->command(SSD1325_SETROWPERIOD);
+  this->command(0x51);
+  this->command(SSD1325_SETPHASELEN);
+  this->command(0x55);
+  this->command(SSD1325_SETPRECHARGECOMP);
+  this->command(0x02);
+  this->command(SSD1325_SETPRECHARGECOMPENABLE);
+  this->command(0x28);
+  this->command(SSD1325_SETVCOMLEVEL);  // Set High Voltage Level of COM Pin
+  this->command(0x1C);                  //?
+  this->command(SSD1325_SETVSL);        // set Low Voltage Level of SEG Pin
+  this->command(0x0D | 0x02);
+  this->command(SSD1325_NORMALDISPLAY); /* set display mode */
+  this->command(SSD1325_DISPLAYON);     /* display ON */
+}
+void SSD1325::display() {
+  this->command(SSD1325_SETCOLADDR); /* set column address */
+  this->command(0x00);               /* set column start address */
+  this->command(0x3F);               /* set column end address */
+  this->command(SSD1325_SETROWADDR); /* set row address */
+  this->command(0x00);               /* set row start address */
+  this->command(0x3F);               /* set row end address */
+
+  this->write_display_data();
+}
+void SSD1325::update() {
+  this->do_update_();
+  this->display();
+}
+int SSD1325::get_height_internal() {
+  switch (this->model_) {
+    case SSD1325_MODEL_128_32:
+      return 32;
+    case SSD1325_MODEL_128_64:
+      return 64;
+    case SSD1325_MODEL_96_16:
+      return 16;
+    case SSD1325_MODEL_64_48:
+      return 48;
+    default:
+      return 0;
+  }
+}
+int SSD1325::get_width_internal() {
+  switch (this->model_) {
+    case SSD1325_MODEL_128_32:
+    case SSD1325_MODEL_128_64:
+      return 128;
+    case SSD1325_MODEL_96_16:
+      return 96;
+    case SSD1325_MODEL_64_48:
+      return 64;
+    default:
+      return 0;
+  }
+}
+size_t SSD1325::get_buffer_length_() {
+  return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u;
+}
+
+void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) {
+  if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0)
+    return;
+
+  uint16_t pos = x + (y / 8) * this->get_width_internal();
+  uint8_t subpos = y % 8;
+  if (color) {
+    this->buffer_[pos] |= (1 << subpos);
+  } else {
+    this->buffer_[pos] &= ~(1 << subpos);
+  }
+}
+void SSD1325::fill(int color) {
+  uint8_t fill = color ? 0xFF : 0x00;
+  for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
+    this->buffer_[i] = fill;
+}
+void SSD1325::init_reset_() {
+  if (this->reset_pin_ != nullptr) {
+    this->reset_pin_->setup();
+    this->reset_pin_->digital_write(true);
+    delay(1);
+    // Trigger Reset
+    this->reset_pin_->digital_write(false);
+    delay(10);
+    // Wake up
+    this->reset_pin_->digital_write(true);
+  }
+}
+const char *SSD1325::model_str_() {
+  switch (this->model_) {
+    case SSD1325_MODEL_128_32:
+      return "SSD1325 128x32";
+    case SSD1325_MODEL_128_64:
+      return "SSD1325 128x64";
+    case SSD1325_MODEL_96_16:
+      return "SSD1325 96x16";
+    case SSD1325_MODEL_64_48:
+      return "SSD1325 64x48";
+    default:
+      return "Unknown";
+  }
+}
+
+}  // namespace ssd1325_base
+}  // namespace esphome
diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h
new file mode 100644
index 0000000000..e227f68f86
--- /dev/null
+++ b/esphome/components/ssd1325_base/ssd1325_base.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/esphal.h"
+#include "esphome/components/display/display_buffer.h"
+
+namespace esphome {
+namespace ssd1325_base {
+
+enum SSD1325Model {
+  SSD1325_MODEL_128_32 = 0,
+  SSD1325_MODEL_128_64,
+  SSD1325_MODEL_96_16,
+  SSD1325_MODEL_64_48,
+};
+
+class SSD1325 : public PollingComponent, public display::DisplayBuffer {
+ public:
+  void setup() override;
+
+  void display();
+
+  void update() override;
+
+  void set_model(SSD1325Model model) { this->model_ = model; }
+  void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
+  void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; }
+
+  float get_setup_priority() const override { return setup_priority::PROCESSOR; }
+  void fill(int color) override;
+
+ protected:
+  virtual void command(uint8_t value) = 0;
+  virtual void write_display_data() = 0;
+  void init_reset_();
+
+  void draw_absolute_pixel_internal(int x, int y, int color) override;
+
+  int get_height_internal() override;
+  int get_width_internal() override;
+  size_t get_buffer_length_();
+  const char *model_str_();
+
+  SSD1325Model model_{SSD1325_MODEL_128_64};
+  GPIOPin *reset_pin_{nullptr};
+  bool external_vcc_{false};
+};
+
+}  // namespace ssd1325_base
+}  // namespace esphome
diff --git a/esphome/components/ssd1325_spi/__init__.py b/esphome/components/ssd1325_spi/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py
new file mode 100644
index 0000000000..4615d45393
--- /dev/null
+++ b/esphome/components/ssd1325_spi/display.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import spi, ssd1325_base
+from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
+
+AUTO_LOAD = ['ssd1325_base']
+DEPENDENCIES = ['spi']
+
+ssd1325_spi = cg.esphome_ns.namespace('ssd1325_spi')
+SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevice)
+
+CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(SPISSD1325),
+    cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
+}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA),
+                       cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield ssd1325_base.setup_ssd1036(var, config)
+    yield spi.register_spi_device(var, config)
+
+    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp
new file mode 100644
index 0000000000..399700f1dd
--- /dev/null
+++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp
@@ -0,0 +1,64 @@
+#include "ssd1325_spi.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace ssd1325_spi {
+
+static const char *TAG = "ssd1325_spi";
+
+void SPISSD1325::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325...");
+  this->spi_setup();
+  this->dc_pin_->setup();  // OUTPUT
+  this->cs_->setup();      // OUTPUT
+
+  this->init_reset_();
+  delay(500);  // NOLINT
+  SSD1325::setup();
+}
+void SPISSD1325::dump_config() {
+  LOG_DISPLAY("", "SPI SSD1325", this);
+  ESP_LOGCONFIG(TAG, "  Model: %s", this->model_str_());
+  LOG_PIN("  CS Pin: ", this->cs_);
+  LOG_PIN("  DC Pin: ", this->dc_pin_);
+  LOG_PIN("  Reset Pin: ", this->reset_pin_);
+  ESP_LOGCONFIG(TAG, "  External VCC: %s", YESNO(this->external_vcc_));
+  LOG_UPDATE_INTERVAL(this);
+}
+void SPISSD1325::command(uint8_t value) {
+  this->cs_->digital_write(true);
+  this->dc_pin_->digital_write(false);
+  delay(1);
+  this->enable();
+  this->cs_->digital_write(false);
+  this->write_byte(value);
+  this->cs_->digital_write(true);
+  this->disable();
+}
+void HOT SPISSD1325::write_display_data() {
+  this->cs_->digital_write(true);
+  this->dc_pin_->digital_write(true);
+  this->cs_->digital_write(false);
+  delay(1);
+  this->enable();
+  for (uint16_t x = 0; x < this->get_width_internal(); x += 2) {
+    for (uint16_t y = 0; y < this->get_height_internal(); y += 8) {  // we write 8 pixels at once
+      uint8_t left8 = this->buffer_[y * 16 + x];
+      uint8_t right8 = this->buffer_[y * 16 + x + 1];
+      for (uint8_t p = 0; p < 8; p++) {
+        uint8_t d = 0;
+        if (left8 & (1 << p))
+          d |= 0xF0;
+        if (right8 & (1 << p))
+          d |= 0x0F;
+        this->write_byte(d);
+      }
+    }
+  }
+  this->cs_->digital_write(true);
+  this->disable();
+}
+
+}  // namespace ssd1325_spi
+}  // namespace esphome
diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.h b/esphome/components/ssd1325_spi/ssd1325_spi.h
new file mode 100644
index 0000000000..e4e7d55769
--- /dev/null
+++ b/esphome/components/ssd1325_spi/ssd1325_spi.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/ssd1325_base/ssd1325_base.h"
+#include "esphome/components/spi/spi.h"
+
+namespace esphome {
+namespace ssd1325_spi {
+
+class SPISSD1325 : public ssd1325_base::SSD1325,
+                   public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
+                                         spi::DATA_RATE_8MHZ> {
+ public:
+  void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
+
+  void setup() override;
+
+  void dump_config() override;
+
+ protected:
+  void command(uint8_t value) override;
+
+  void write_display_data() override;
+
+  GPIOPin *dc_pin_;
+};
+
+}  // namespace ssd1325_spi
+}  // namespace esphome
diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp
index 7fbeb8c171..90ac1faad7 100644
--- a/esphome/components/status/status_binary_sensor.cpp
+++ b/esphome/components/status/status_binary_sensor.cpp
@@ -30,7 +30,7 @@ void StatusBinarySensor::loop() {
 
   this->publish_state(status);
 }
-void StatusBinarySensor::setup() { this->publish_state(false); }
+void StatusBinarySensor::setup() { this->publish_initial_state(false); }
 void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); }
 
 }  // namespace status
diff --git a/esphome/components/sts3x/__init__.py b/esphome/components/sts3x/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py
new file mode 100644
index 0000000000..f48deeeae5
--- /dev/null
+++ b/esphome/components/sts3x/sensor.py
@@ -0,0 +1,22 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS
+
+DEPENDENCIES = ['i2c']
+
+sts3x_ns = cg.esphome_ns.namespace('sts3x')
+
+STS3XComponent = sts3x_ns.class_('STS3XComponent', sensor.Sensor,
+                                 cg.PollingComponent, i2c.I2CDevice)
+
+CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({
+    cv.GenerateID(): cv.declare_id(STS3XComponent),
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x4A))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield sensor.register_sensor(var, config)
+    yield i2c.register_i2c_device(var, config)
diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp
new file mode 100644
index 0000000000..1a24a17caf
--- /dev/null
+++ b/esphome/components/sts3x/sts3x.cpp
@@ -0,0 +1,123 @@
+#include "sts3x.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sts3x {
+
+static const char *TAG = "sts3x";
+
+static const uint16_t STS3X_COMMAND_READ_SERIAL_NUMBER = 0x3780;
+static const uint16_t STS3X_COMMAND_READ_STATUS = 0xF32D;
+static const uint16_t STS3X_COMMAND_SOFT_RESET = 0x30A2;
+static const uint16_t STS3X_COMMAND_POLLING_H = 0x2400;
+
+/// Commands for future use
+static const uint16_t STS3X_COMMAND_CLEAR_STATUS = 0x3041;
+static const uint16_t STS3X_COMMAND_HEATER_ENABLE = 0x306D;
+static const uint16_t STS3X_COMMAND_HEATER_DISABLE = 0x3066;
+static const uint16_t STS3X_COMMAND_FETCH_DATA = 0xE000;
+
+void STS3XComponent::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up STS3x...");
+  if (!this->write_command_(STS3X_COMMAND_READ_SERIAL_NUMBER)) {
+    this->mark_failed();
+    return;
+  }
+
+  uint16_t raw_serial_number[2];
+  if (!this->read_data_(raw_serial_number, 1)) {
+    this->mark_failed();
+    return;
+  }
+  uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16);
+  ESP_LOGV(TAG, "    Serial Number: 0x%08X", serial_number);
+}
+void STS3XComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "STS3x:");
+  LOG_I2C_DEVICE(this);
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Communication with ST3x failed!");
+  }
+  LOG_UPDATE_INTERVAL(this);
+
+  LOG_SENSOR("  ", "STS3x", this);
+}
+float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; }
+void STS3XComponent::update() {
+  if (this->status_has_warning()) {
+    ESP_LOGD(TAG, "Retrying to reconnect the sensor.");
+    this->write_command_(STS3X_COMMAND_SOFT_RESET);
+  }
+  if (!this->write_command_(STS3X_COMMAND_POLLING_H)) {
+    this->status_set_warning();
+    return;
+  }
+
+  this->set_timeout(50, [this]() {
+    uint16_t raw_data[1];
+    if (!this->read_data_(raw_data, 1)) {
+      this->status_set_warning();
+      return;
+    }
+
+    float temperature = 175.0f * float(raw_data[0]) / 65535.0f - 45.0f;
+    ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature);
+    this->publish_state(temperature);
+    this->status_clear_warning();
+  });
+}
+
+bool STS3XComponent::write_command_(uint16_t command) {
+  // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
+  return this->write_byte(command >> 8, command & 0xFF);
+}
+
+uint8_t sts3x_crc(uint8_t data1, uint8_t data2) {
+  uint8_t bit;
+  uint8_t crc = 0xFF;
+
+  crc ^= data1;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  crc ^= data2;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  return crc;
+}
+
+bool STS3XComponent::read_data_(uint16_t *data, uint8_t len) {
+  const uint8_t num_bytes = len * 3;
+  auto *buf = new uint8_t[num_bytes];
+
+  if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) {
+    delete[](buf);
+    return false;
+  }
+
+  for (uint8_t i = 0; i < len; i++) {
+    const uint8_t j = 3 * i;
+    uint8_t crc = sts3x_crc(buf[j], buf[j + 1]);
+    if (crc != buf[j + 2]) {
+      ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
+      delete[](buf);
+      return false;
+    }
+    data[i] = (buf[j] << 8) | buf[j + 1];
+  }
+
+  delete[](buf);
+  return true;
+}
+
+}  // namespace sts3x
+}  // namespace esphome
diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h
new file mode 100644
index 0000000000..436cf938d8
--- /dev/null
+++ b/esphome/components/sts3x/sts3x.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace sts3x {
+
+/// This class implements support for the ST3x-DIS family of temperature i2c sensors.
+class STS3XComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void update() override;
+
+ protected:
+  bool write_command_(uint16_t command);
+  bool read_data_(uint16_t *data, uint8_t len);
+};
+
+}  // namespace sts3x
+}  // namespace esphome
diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py
index 0bcdedcabd..fef0902181 100644
--- a/esphome/components/sun/__init__.py
+++ b/esphome/components/sun/__init__.py
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
 from esphome import automation
 from esphome.components import time
 from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID
+from esphome.py_compat import string_types
 
 sun_ns = cg.esphome_ns.namespace('sun')
 
@@ -31,9 +32,9 @@ ELEVATION_MAP = {
 
 
 def elevation(value):
-    if isinstance(value, str):
+    if isinstance(value, string_types):
         try:
-            value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')]
+            value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)]
         except cv.Invalid:
             pass
     value = cv.angle(value)
diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h
index be4fc24c4a..cd6cec429f 100644
--- a/esphome/components/switch/switch.h
+++ b/esphome/components/switch/switch.h
@@ -9,15 +9,15 @@ namespace switch_ {
 
 #define LOG_SWITCH(prefix, type, obj) \
   if (obj != nullptr) { \
-    ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
     if (!obj->get_icon().empty()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Icon: '%s'", obj->get_icon().c_str()); \
+      ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, obj->get_icon().c_str()); \
     } \
     if (obj->assumed_state()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Assumed State: YES"); \
+      ESP_LOGCONFIG(TAG, "%s  Assumed State: YES", prefix); \
     } \
     if (obj->is_inverted()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Inverted: YES"); \
+      ESP_LOGCONFIG(TAG, "%s  Inverted: YES", prefix); \
     } \
   }
 
diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py
new file mode 100644
index 0000000000..11fcfe3955
--- /dev/null
+++ b/esphome/components/sx1509/__init__.py
@@ -0,0 +1,77 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import i2c
+from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED
+
+CONF_KEYPAD = 'keypad'
+CONF_KEY_ROWS = 'key_rows'
+CONF_KEY_COLUMNS = 'key_columns'
+CONF_SLEEP_TIME = 'sleep_time'
+CONF_SCAN_TIME = 'scan_time'
+CONF_DEBOUNCE_TIME = 'debounce_time'
+
+DEPENDENCIES = ['i2c']
+MULTI_CONF = True
+
+sx1509_ns = cg.esphome_ns.namespace('sx1509')
+SX1509GPIOMode = sx1509_ns.enum('SX1509GPIOMode')
+SX1509_GPIO_MODES = {
+    'INPUT': SX1509GPIOMode.SX1509_INPUT,
+    'INPUT_PULLUP': SX1509GPIOMode.SX1509_INPUT_PULLUP,
+    'OUTPUT': SX1509GPIOMode.SX1509_OUTPUT
+}
+
+SX1509Component = sx1509_ns.class_('SX1509Component', cg.Component, i2c.I2CDevice)
+SX1509GPIOPin = sx1509_ns.class_('SX1509GPIOPin', cg.GPIOPin)
+
+KEYPAD_SCHEMA = cv.Schema({
+    cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8),
+    cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8),
+    cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192),
+    cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128),
+    cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64),
+})
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SX1509Component),
+    cv.Optional(CONF_KEYPAD): cv.Schema(KEYPAD_SCHEMA),
+}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x3E))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+    if CONF_KEYPAD in config:
+        keypad = config[CONF_KEYPAD]
+        cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS]))
+        if CONF_SLEEP_TIME in keypad and CONF_SCAN_TIME in keypad and CONF_DEBOUNCE_TIME in keypad:
+            cg.add(var.set_sleep_time(keypad[CONF_SLEEP_TIME]))
+            cg.add(var.set_scan_time(keypad[CONF_SCAN_TIME]))
+            cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME]))
+
+
+CONF_SX1509 = 'sx1509'
+CONF_SX1509_ID = 'sx1509_id'
+
+SX1509_OUTPUT_PIN_SCHEMA = cv.Schema({
+    cv.Required(CONF_SX1509): cv.use_id(SX1509Component),
+    cv.Required(CONF_NUMBER): cv.int_,
+    cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(SX1509_GPIO_MODES, upper=True),
+    cv.Optional(CONF_INVERTED, default=False): cv.boolean,
+})
+SX1509_INPUT_PIN_SCHEMA = cv.Schema({
+    cv.Required(CONF_SX1509): cv.use_id(SX1509Component),
+    cv.Required(CONF_NUMBER): cv.int_,
+    cv.Optional(CONF_MODE, default="INPUT"): cv.enum(SX1509_GPIO_MODES, upper=True),
+    cv.Optional(CONF_INVERTED, default=False): cv.boolean,
+})
+
+
+@pins.PIN_SCHEMA_REGISTRY.register(CONF_SX1509,
+                                   (SX1509_OUTPUT_PIN_SCHEMA, SX1509_INPUT_PIN_SCHEMA))
+def sx1509_pin_to_code(config):
+    parent = yield cg.get_variable(config[CONF_SX1509])
+    yield SX1509GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE],
+                            config[CONF_INVERTED])
diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py
new file mode 100644
index 0000000000..9a65524383
--- /dev/null
+++ b/esphome/components/sx1509/binary_sensor/__init__.py
@@ -0,0 +1,28 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from esphome.const import CONF_ID
+from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID
+
+CONF_ROW = 'row'
+CONF_COL = 'col'
+
+DEPENDENCIES = ['sx1509']
+
+SX1509BinarySensor = sx1509_ns.class_('SX1509BinarySensor', binary_sensor.BinarySensor)
+
+CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(SX1509BinarySensor),
+    cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component),
+    cv.Required(CONF_ROW): cv.int_range(min=0, max=4),
+    cv.Required(CONF_COL): cv.int_range(min=0, max=4),
+})
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield binary_sensor.register_binary_sensor(var, config)
+    hub = yield cg.get_variable(config[CONF_SX1509_ID])
+    cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL]))
+
+    cg.add(hub.register_keypad_binary_sensor(var))
diff --git a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h
new file mode 100644
index 0000000000..2eef19782c
--- /dev/null
+++ b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "esphome/components/sx1509/sx1509.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace sx1509 {
+
+class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor::BinarySensor {
+ public:
+  void set_row_col(uint8_t row, uint8_t col) { this->key_ = (1 << (col + 8)) | (1 << row); }
+  void process(uint16_t data) override { this->publish_state(static_cast<bool>(data == key_)); }
+
+ protected:
+  uint16_t key_{0};
+};
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/output/__init__.py b/esphome/components/sx1509/output/__init__.py
new file mode 100644
index 0000000000..80aec0afd4
--- /dev/null
+++ b/esphome/components/sx1509/output/__init__.py
@@ -0,0 +1,25 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_PIN, CONF_ID
+from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID
+
+DEPENDENCIES = ['sx1509']
+
+SX1509FloatOutputChannel = sx1509_ns.class_('SX1509FloatOutputChannel',
+                                            output.FloatOutput, cg.Component)
+
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
+    cv.Required(CONF_ID): cv.declare_id(SX1509FloatOutputChannel),
+    cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component),
+    cv.Required(CONF_PIN): cv.int_range(min=0, max=15),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    parent = yield cg.get_variable(config[CONF_SX1509_ID])
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield output.register_output(var, config)
+    cg.add(var.set_pin(config[CONF_PIN]))
+    cg.add(var.set_parent(parent))
diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp
new file mode 100644
index 0000000000..7ff1bbb61b
--- /dev/null
+++ b/esphome/components/sx1509/output/sx1509_float_output.cpp
@@ -0,0 +1,30 @@
+#include "sx1509_float_output.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sx1509 {
+
+static const char *TAG = "sx1509_float_channel";
+
+void SX1509FloatOutputChannel::write_state(float state) {
+  const uint16_t max_duty = 255;
+  const float duty_rounded = roundf(state * max_duty);
+  auto duty = static_cast<uint16_t>(duty_rounded);
+  this->parent_->set_pin_value(this->pin_, duty);
+}
+
+void SX1509FloatOutputChannel::setup() {
+  ESP_LOGD(TAG, "setup pin %d", this->pin_);
+  this->parent_->pin_mode(this->pin_, SX1509_ANALOG_OUTPUT);
+  this->turn_off();
+}
+
+void SX1509FloatOutputChannel::dump_config() {
+  ESP_LOGCONFIG(TAG, "SX1509 PWM:");
+  ESP_LOGCONFIG(TAG, "  sx1509 pin: %d", this->pin_);
+  LOG_FLOAT_OUTPUT(this);
+}
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/output/sx1509_float_output.h b/esphome/components/sx1509/output/sx1509_float_output.h
new file mode 100644
index 0000000000..39e51839ea
--- /dev/null
+++ b/esphome/components/sx1509/output/sx1509_float_output.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "esphome/components/sx1509/sx1509.h"
+#include "esphome/components/output/float_output.h"
+
+namespace esphome {
+namespace sx1509 {
+
+class SX1509Component;
+
+class SX1509FloatOutputChannel : public output::FloatOutput, public Component {
+ public:
+  void set_parent(SX1509Component *parent) { this->parent_ = parent; }
+  void set_pin(uint8_t pin) { pin_ = pin; }
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+ protected:
+  void write_state(float state) override;
+
+  SX1509Component *parent_;
+  uint8_t pin_;
+};
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp
new file mode 100644
index 0000000000..2806a1cac2
--- /dev/null
+++ b/esphome/components/sx1509/sx1509.cpp
@@ -0,0 +1,253 @@
+#include "sx1509.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sx1509 {
+
+static const char *TAG = "sx1509";
+
+void SX1509Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up SX1509Component...");
+
+  ESP_LOGV(TAG, "  Resetting devices...");
+  if (!this->write_byte(REG_RESET, 0x12)) {
+    this->mark_failed();
+    return;
+  }
+  this->write_byte(REG_RESET, 0x34);
+
+  uint16_t data;
+  this->read_byte_16(REG_INTERRUPT_MASK_A, &data);
+  if (data == 0xFF00) {
+    clock_(INTERNAL_CLOCK_2MHZ);
+  } else {
+    this->mark_failed();
+    return;
+  }
+  delayMicroseconds(500);
+  if (this->has_keypad_)
+    this->setup_keypad_();
+}
+
+void SX1509Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "SX1509:");
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Setting up SX1509 failed!");
+  }
+  LOG_I2C_DEVICE(this);
+}
+
+void SX1509Component::loop() {
+  if (this->has_keypad_) {
+    uint16_t key_data = this->read_key_data();
+    for (auto *binary_sensor : this->keypad_binary_sensors_)
+      binary_sensor->process(key_data);
+  }
+}
+
+bool SX1509Component::digital_read(uint8_t pin) {
+  if (this->ddr_mask_ & (1 << pin)) {
+    uint16_t temp_reg_data;
+    this->read_byte_16(REG_DATA_B, &temp_reg_data);
+    if (temp_reg_data & (1 << pin))
+      return true;
+  }
+  return false;
+}
+
+void SX1509Component::digital_write(uint8_t pin, bool bit_value) {
+  if ((~this->ddr_mask_) & (1 << pin)) {
+    // If the pin is an output, write high/low
+    uint16_t temp_reg_data = 0;
+    this->read_byte_16(REG_DATA_B, &temp_reg_data);
+    if (bit_value)
+      temp_reg_data |= (1 << pin);
+    else
+      temp_reg_data &= ~(1 << pin);
+    this->write_byte_16(REG_DATA_B, temp_reg_data);
+  } else {
+    // Otherwise the pin is an input, pull-up/down
+    uint16_t temp_pullup;
+    this->read_byte_16(REG_PULL_UP_B, &temp_pullup);
+    uint16_t temp_pull_down;
+    this->read_byte_16(REG_PULL_DOWN_B, &temp_pull_down);
+
+    if (bit_value) {
+      // if HIGH, do pull-up, disable pull-down
+      temp_pullup |= (1 << pin);
+      temp_pull_down &= ~(1 << pin);
+      this->write_byte_16(REG_PULL_UP_B, temp_pullup);
+      this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down);
+    } else {
+      // If LOW do pull-down, disable pull-up
+      temp_pull_down |= (1 << pin);
+      temp_pullup &= ~(1 << pin);
+      this->write_byte_16(REG_PULL_UP_B, temp_pullup);
+      this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down);
+    }
+  }
+}
+
+void SX1509Component::pin_mode(uint8_t pin, uint8_t mode) {
+  this->read_byte_16(REG_DIR_B, &this->ddr_mask_);
+  if ((mode == SX1509_OUTPUT) || (mode == SX1509_ANALOG_OUTPUT))
+    this->ddr_mask_ &= ~(1 << pin);
+  else
+    this->ddr_mask_ |= (1 << pin);
+  this->write_byte_16(REG_DIR_B, this->ddr_mask_);
+
+  if (mode == INPUT_PULLUP)
+    digital_write(pin, HIGH);
+
+  if (mode == SX1509_ANALOG_OUTPUT) {
+    setup_led_driver_(pin);
+  }
+}
+
+void SX1509Component::setup_led_driver_(uint8_t pin) {
+  uint16_t temp_word;
+  uint8_t temp_byte;
+
+  this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word);
+  temp_word |= (1 << pin);
+  this->write_byte_16(REG_INPUT_DISABLE_B, temp_word);
+
+  this->read_byte_16(REG_PULL_UP_B, &temp_word);
+  temp_word &= ~(1 << pin);
+  this->write_byte_16(REG_PULL_UP_B, temp_word);
+
+  this->ddr_mask_ &= ~(1 << pin);  // 0=output
+  this->write_byte_16(REG_DIR_B, this->ddr_mask_);
+
+  this->read_byte(REG_CLOCK, &temp_byte);
+  temp_byte |= (1 << 6);   // Internal 2MHz oscillator part 1 (set bit 6)
+  temp_byte &= ~(1 << 5);  // Internal 2MHz oscillator part 2 (clear bit 5)
+  this->write_byte(REG_CLOCK, temp_byte);
+
+  this->read_byte(REG_MISC, &temp_byte);
+  temp_byte &= ~(1 << 7);  // set linear mode bank B
+  temp_byte &= ~(1 << 3);  // set linear mode bank A
+  temp_byte |= 0x70;       // Frequency of the LED Driver clock ClkX of all IOs:
+  this->write_byte(REG_MISC, temp_byte);
+
+  this->read_byte_16(REG_LED_DRIVER_ENABLE_B, &temp_word);
+  temp_word |= (1 << pin);
+  this->write_byte_16(REG_LED_DRIVER_ENABLE_B, temp_word);
+
+  this->read_byte_16(REG_DATA_B, &temp_word);
+  temp_word &= ~(1 << pin);
+  this->write_byte_16(REG_DATA_B, temp_word);
+}
+
+void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_freq_out, byte osc_divider) {
+  osc_source = (osc_source & 0b11) << 5;           // 2-bit value, bits 6:5
+  osc_pin_function = (osc_pin_function & 1) << 4;  // 1-bit value bit 4
+  osc_freq_out = (osc_freq_out & 0b1111);          // 4-bit value, bits 3:0
+  byte reg_clock = osc_source | osc_pin_function | osc_freq_out;
+  this->write_byte(REG_CLOCK, reg_clock);
+
+  osc_divider = constrain(osc_divider, 1, 7);
+  this->clk_x_ = 2000000;
+  osc_divider = (osc_divider & 0b111) << 4;  // 3-bit value, bits 6:4
+
+  uint8_t reg_misc;
+  this->read_byte(REG_MISC, &reg_misc);
+  reg_misc &= ~(0b111 << 4);
+  reg_misc |= osc_divider;
+  this->write_byte(REG_MISC, reg_misc);
+}
+
+void SX1509Component::setup_keypad_() {
+  uint8_t temp_byte;
+
+  // setup row/col pins for INPUT OUTPUT
+  this->read_byte_16(REG_DIR_B, &this->ddr_mask_);
+  for (int i = 0; i < this->rows_; i++)
+    this->ddr_mask_ &= ~(1 << i);
+  for (int i = 8; i < (this->cols_ * 2); i++)
+    this->ddr_mask_ |= (1 << i);
+  this->write_byte_16(REG_DIR_B, this->ddr_mask_);
+
+  this->read_byte(REG_OPEN_DRAIN_A, &temp_byte);
+  for (int i = 0; i < this->rows_; i++)
+    temp_byte |= (1 << i);
+  this->write_byte(REG_OPEN_DRAIN_A, temp_byte);
+
+  this->read_byte(REG_PULL_UP_B, &temp_byte);
+  for (int i = 0; i < this->cols_; i++)
+    temp_byte |= (1 << i);
+  this->write_byte(REG_PULL_UP_B, temp_byte);
+
+  if (debounce_time_ >= scan_time_) {
+    debounce_time_ = scan_time_ >> 1;  // Force debounce_time to be less than scan_time
+  }
+  set_debounce_keypad_(debounce_time_, rows_, cols_);
+  uint8_t scan_time_bits = 0;
+  for (uint8_t i = 7; i > 0; i--) {
+    if (scan_time_ & (1 << i)) {
+      scan_time_bits = i;
+      break;
+    }
+  }
+  scan_time_bits &= 0b111;  // Scan time is bits 2:0
+  temp_byte = sleep_time_ | scan_time_bits;
+  this->write_byte(REG_KEY_CONFIG_1, temp_byte);
+  rows_ = (rows_ - 1) & 0b111;  // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc.
+  cols_ = (cols_ - 1) & 0b111;  // 0b000 = 1 column, ob111 = 8 columns, etc.
+  this->write_byte(REG_KEY_CONFIG_2, (rows_ << 3) | cols_);
+}
+
+uint16_t SX1509Component::read_key_data() {
+  uint16_t key_data;
+  this->read_byte_16(REG_KEY_DATA_1, &key_data);
+  return (0xFFFF ^ key_data);
+}
+
+void SX1509Component::set_debounce_config_(uint8_t config_value) {
+  // First make sure clock is configured
+  uint8_t temp_byte;
+  this->read_byte(REG_MISC, &temp_byte);
+  temp_byte |= (1 << 4);  // Just default to no divider if not set
+  this->write_byte(REG_MISC, temp_byte);
+  this->read_byte(REG_CLOCK, &temp_byte);
+  temp_byte |= (1 << 6);  // default to internal osc.
+  this->write_byte(REG_CLOCK, temp_byte);
+
+  config_value &= 0b111;  // 3-bit value
+  this->write_byte(REG_DEBOUNCE_CONFIG, config_value);
+}
+
+void SX1509Component::set_debounce_time_(uint8_t time) {
+  uint8_t config_value = 0;
+
+  for (int i = 7; i >= 0; i--) {
+    if (time & (1 << i)) {
+      config_value = i + 1;
+      break;
+    }
+  }
+  config_value = constrain(config_value, 0, 7);
+
+  set_debounce_config_(config_value);
+}
+
+void SX1509Component::set_debounce_enable_(uint8_t pin) {
+  uint16_t debounce_enable;
+  this->read_byte_16(REG_DEBOUNCE_ENABLE_B, &debounce_enable);
+  debounce_enable |= (1 << pin);
+  this->write_byte_16(REG_DEBOUNCE_ENABLE_B, debounce_enable);
+}
+
+void SX1509Component::set_debounce_pin_(uint8_t pin) { set_debounce_enable_(pin); }
+
+void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols) {
+  set_debounce_time_(time);
+  for (uint16_t i = 0; i < num_rows; i++)
+    set_debounce_pin_(i);
+  for (uint16_t i = 0; i < (8 + num_cols); i++)
+    set_debounce_pin_(i);
+}
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h
new file mode 100644
index 0000000000..55d5e54091
--- /dev/null
+++ b/esphome/components/sx1509/sx1509.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/core/component.h"
+#include "sx1509_gpio_pin.h"
+#include "sx1509_registers.h"
+
+namespace esphome {
+namespace sx1509 {
+
+// These are used for clock config:
+const uint8_t INTERNAL_CLOCK_2MHZ = 2;
+const uint8_t EXTERNAL_CLOCK = 1;
+const uint8_t SOFTWARE_RESET = 0;
+const uint8_t HARDWARE_RESET = 1;
+
+const uint8_t ANALOG_OUTPUT = 0x03;  // To set a pin mode for PWM output
+
+// PinModes for SX1509 pins
+enum SX1509GPIOMode : uint8_t {
+  SX1509_INPUT = INPUT,                  // 0x00
+  SX1509_INPUT_PULLUP = INPUT_PULLUP,    // 0x02
+  SX1509_ANALOG_OUTPUT = ANALOG_OUTPUT,  // 0x03
+  SX1509_OUTPUT = OUTPUT,                // 0x01
+};
+
+const uint8_t REG_I_ON[16] = {REG_I_ON_0,  REG_I_ON_1,  REG_I_ON_2,  REG_I_ON_3, REG_I_ON_4,  REG_I_ON_5,
+                              REG_I_ON_6,  REG_I_ON_7,  REG_I_ON_8,  REG_I_ON_9, REG_I_ON_10, REG_I_ON_11,
+                              REG_I_ON_12, REG_I_ON_13, REG_I_ON_14, REG_I_ON_15};
+
+// for all components that implement the process(uint16_t data )
+class SX1509Processor {
+ public:
+  virtual void process(uint16_t data){};
+};
+
+class SX1509Component : public Component, public i2c::I2CDevice {
+ public:
+  SX1509Component() = default;
+
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+  void loop() override;
+
+  bool digital_read(uint8_t pin);
+  uint16_t read_key_data();
+  void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); };
+  void pin_mode(uint8_t pin, uint8_t mode);
+  void digital_write(uint8_t pin, bool bit_value);
+  u_long get_clock() { return this->clk_x_; };
+  void set_rows_cols(uint8_t rows, uint8_t cols) {
+    this->rows_ = rows;
+    this->cols_ = cols;
+    this->has_keypad_ = true;
+  };
+  void set_sleep_time(uint16_t sleep_time) { this->sleep_time_ = sleep_time; };
+  void set_scan_time(uint8_t scan_time) { this->scan_time_ = scan_time; };
+  void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; };
+  void register_keypad_binary_sensor(SX1509Processor *binary_sensor) {
+    this->keypad_binary_sensors_.push_back(binary_sensor);
+  };
+
+ protected:
+  u_long clk_x_ = 2000000;
+  uint8_t frequency_ = 0;
+  uint16_t ddr_mask_ = 0x00;
+  uint16_t input_mask_ = 0x00;
+  uint16_t port_mask_ = 0x00;
+  bool has_keypad_ = false;
+  uint8_t rows_ = 0;
+  uint8_t cols_ = 0;
+  uint16_t sleep_time_ = 128;
+  uint8_t scan_time_ = 1;
+  uint8_t debounce_time_ = 1;
+  std::vector<SX1509Processor *> keypad_binary_sensors_;
+
+  void setup_keypad_();
+  void set_debounce_config_(uint8_t config_value);
+  void set_debounce_time_(uint8_t time);
+  void set_debounce_pin_(uint8_t pin);
+  void set_debounce_enable_(uint8_t pin);
+  void set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols);
+  void setup_led_driver_(uint8_t pin);
+  void clock_(uint8_t osc_source = 2, uint8_t osc_pin_function = 1, uint8_t osc_freq_out = 0, uint8_t osc_divider = 0);
+};
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp
new file mode 100644
index 0000000000..1d1c87b4e6
--- /dev/null
+++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp
@@ -0,0 +1,20 @@
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+#include "sx1509_gpio_pin.h"
+
+namespace esphome {
+namespace sx1509 {
+
+static const char *TAG = "sx1509_gpio_pin";
+
+void SX1509GPIOPin::setup() {
+  ESP_LOGD(TAG, "setup pin %d", this->pin_);
+  this->parent_->pin_mode(this->pin_, this->mode_);
+}
+
+void SX1509GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); }
+bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
+void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h
new file mode 100644
index 0000000000..39f841a2a4
--- /dev/null
+++ b/esphome/components/sx1509/sx1509_gpio_pin.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "sx1509.h"
+
+namespace esphome {
+namespace sx1509 {
+
+class SX1509Component;
+
+class SX1509GPIOPin : public GPIOPin {
+ public:
+  SX1509GPIOPin(SX1509Component *parent, uint8_t pin, uint8_t mode, bool inverted = false)
+      : GPIOPin(pin, mode, inverted), parent_(parent){};
+  void setup() override;
+  void pin_mode(uint8_t mode) override;
+  bool digital_read() override;
+  void digital_write(bool value) override;
+
+ protected:
+  SX1509Component *parent_;
+};
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h
new file mode 100644
index 0000000000..d73f397f16
--- /dev/null
+++ b/esphome/components/sx1509/sx1509_registers.h
@@ -0,0 +1,109 @@
+/******************************************************************************
+sx1509_registers.h
+Register definitions for SX1509.
+Jim Lindblom @ SparkFun Electronics
+Original Creation Date: September 21, 2015
+https://github.com/sparkfun/SparkFun_SX1509_Arduino_Library
+
+Here you'll find the Arduino code used to interface with the SX1509 I2C
+16 I/O expander. There are functions to take advantage of everything the
+SX1509 provides - input/output setting, writing pins high/low, reading
+the input value of pins, LED driver utilities (blink, breath, pwm), and
+keypad engine utilites.
+
+Development environment specifics:
+    IDE: Arduino 1.6.5
+    Hardware Platform: Arduino Uno
+    SX1509 Breakout Version: v2.0
+
+This code is beerware; if you see me (or any other SparkFun employee) at the
+local, and you've found our code helpful, please buy us a round!
+
+Distributed as-is; no warranty is given.
+******************************************************************************/
+#pragma once
+
+namespace esphome {
+namespace sx1509 {
+
+const uint8_t REG_INPUT_DISABLE_B =
+    0x00;  //    RegInputDisableB Input buffer disable register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_INPUT_DISABLE_A =
+    0x01;  //    RegInputDisableA Input buffer disable register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_LONG_SLEW_B =
+    0x02;  //    RegLongSlewB Output buffer long slew register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_LONG_SLEW_A = 0x03;  //    RegLongSlewA Output buffer long slew register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_LOW_DRIVE_B =
+    0x04;  //    RegLowDriveB Output buffer low drive register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_LOW_DRIVE_A = 0x05;  //    RegLowDriveA Output buffer low drive register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_PULL_UP_B = 0x06;    //    RegPullUpB Pull_up register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_PULL_UP_A = 0x07;    //    RegPullUpA Pull_up register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_PULL_DOWN_B = 0x08;  //    RegPullDownB Pull_down register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_PULL_DOWN_A = 0x09;  //    RegPullDownA Pull_down register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_OPEN_DRAIN_B = 0x0A;  //    RegOpenDrainB Open drain register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_OPEN_DRAIN_A = 0x0B;  //    RegOpenDrainA Open drain register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_POLARITY_B = 0x0C;    //    RegPolarityB Polarity register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_POLARITY_A = 0x0D;    //    RegPolarityA Polarity register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_DIR_B = 0x0E;         //    RegDirB Direction register _ I/O[15_8] (Bank B) 1111 1111
+const uint8_t REG_DIR_A = 0x0F;         //    RegDirA Direction register _ I/O[7_0] (Bank A) 1111 1111
+const uint8_t REG_DATA_B = 0x10;        //    RegDataB Data register _ I/O[15_8] (Bank B) 1111 1111*
+const uint8_t REG_DATA_A = 0x11;        //    RegDataA Data register _ I/O[7_0] (Bank A) 1111 1111*
+const uint8_t REG_INTERRUPT_MASK_B =
+    0x12;  //    RegInterruptMaskB Interrupt mask register _ I/O[15_8] (Bank B) 1111 1111
+const uint8_t REG_INTERRUPT_MASK_A =
+    0x13;                               //    RegInterruptMaskA Interrupt mask register _ I/O[7_0] (Bank A) 1111 1111
+const uint8_t REG_SENSE_HIGH_B = 0x14;  //    RegSenseHighB Sense register for I/O[15:12] 0000 0000
+const uint8_t REG_SENSE_LOW_B = 0x15;   //    RegSenseLowB Sense register for I/O[11:8] 0000 0000
+const uint8_t REG_SENSE_HIGH_A = 0x16;  //    RegSenseHighA Sense register for I/O[7:4] 0000 0000
+const uint8_t REG_SENSE_LOW_A = 0x17;   //    RegSenseLowA Sense register for I/O[3:0] 0000 0000
+const uint8_t REG_INTERRUPT_SOURCE_B =
+    0x18;  //    RegInterruptSourceB Interrupt source register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_INTERRUPT_SOURCE_A =
+    0x19;  //    RegInterruptSourceA Interrupt source register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_EVENT_STATUS_B = 0x1A;   //    RegEventStatusB Event status register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_EVENT_STATUS_A = 0x1B;   //    RegEventStatusA Event status register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_LEVEL_SHIFTER_1 = 0x1C;  //    RegLevelShifter1 Level shifter register 0000 0000
+const uint8_t REG_LEVEL_SHIFTER_2 = 0x1D;  //    RegLevelShifter2 Level shifter register 0000 0000
+const uint8_t REG_CLOCK = 0x1E;            //    RegClock Clock management register 0000 0000
+const uint8_t REG_MISC = 0x1F;             //    RegMisc Miscellaneous device settings register 0000 0000
+const uint8_t REG_LED_DRIVER_ENABLE_B =
+    0x20;  //    RegLEDDriverEnableB LED driver enable register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_LED_DRIVER_ENABLE_A =
+    0x21;  //    RegLEDDriverEnableA LED driver enable register _ I/O[7_0] (Bank A) 0000 0000
+// Debounce and Keypad Engine
+const uint8_t REG_DEBOUNCE_CONFIG = 0x22;  //    RegDebounceConfig Debounce configuration register 0000 0000
+const uint8_t REG_DEBOUNCE_ENABLE_B =
+    0x23;  //    RegDebounceEnableB Debounce enable register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_DEBOUNCE_ENABLE_A =
+    0x24;                               //    RegDebounceEnableA Debounce enable register _ I/O[7_0] (Bank A) 0000 0000
+const uint8_t REG_KEY_CONFIG_1 = 0x25;  //    RegKeyConfig1 Key scan configuration register 0000 0000
+const uint8_t REG_KEY_CONFIG_2 = 0x26;  //    RegKeyConfig2 Key scan configuration register 0000 0000
+const uint8_t REG_KEY_DATA_1 = 0x27;    //    RegKeyData1 Key value (column) 1111 1111
+const uint8_t REG_KEY_DATA_2 = 0x28;    //    RegKeyData2 Key value (row) 1111 1111
+// LED Driver (PWM, blinking, breathing)
+const uint8_t REG_I_ON_0 = 0x2A;   //    RegIOn0 ON intensity register for I/O[0] 1111 1111
+const uint8_t REG_I_ON_1 = 0x2D;   //    RegIOn1 ON intensity register for I/O[1] 1111 1111
+const uint8_t REG_I_ON_2 = 0x30;   //    RegIOn2 ON intensity register for I/O[2] 1111 1111
+const uint8_t REG_I_ON_3 = 0x33;   //    RegIOn3 ON intensity register for I/O[3] 1111 1111
+const uint8_t REG_I_ON_4 = 0x36;   //    RegIOn4 ON intensity register for I/O[4] 1111 1111
+const uint8_t REG_I_ON_5 = 0x3B;   //    RegIOn5 ON intensity register for I/O[5] 1111 1111
+const uint8_t REG_I_ON_6 = 0x40;   //    RegIOn6 ON intensity register for I/O[6] 1111 1111
+const uint8_t REG_I_ON_7 = 0x45;   //    RegIOn7 ON intensity register for I/O[7] 1111 1111
+const uint8_t REG_I_ON_8 = 0x4A;   //    RegIOn8 ON intensity register for I/O[8] 1111 1111
+const uint8_t REG_I_ON_9 = 0x4D;   //    RegIOn9 ON intensity register for I/O[9] 1111 1111
+const uint8_t REG_I_ON_10 = 0x50;  //    RegIOn10 ON intensity register for I/O[10] 1111 1111
+const uint8_t REG_I_ON_11 = 0x53;  //    RegIOn11 ON intensity register for I/O[11] 1111 1111
+const uint8_t REG_I_ON_12 = 0x56;  //    RegIOn12 ON intensity register for I/O[12] 1111 1111
+const uint8_t REG_I_ON_13 = 0x5B;  //    RegIOn13 ON intensity register for I/O[13] 1111 1111
+const uint8_t REG_I_ON_14 = 0x60;  //    RegIOn14 ON intensity register for I/O[14] 1111 1111
+const uint8_t REG_I_ON_15 = 0x65;  //    RegIOn15 ON intensity register for I/O[15] 1111 1111
+//     Miscellaneous
+const uint8_t REG_HIGH_INPUT_B = 0x69;  //    RegHighInputB High input enable register _ I/O[15_8] (Bank B) 0000 0000
+const uint8_t REG_HIGH_INPUT_A = 0x6A;  //    RegHighInputA High input enable register _ I/O[7_0] (Bank A) 0000 0000
+//  Software Reset
+const uint8_t REG_RESET = 0x7D;   //    RegReset Software reset register 0000 0000
+const uint8_t REG_TEST_1 = 0x7E;  //    RegTest1 Test register 0000 0000
+const uint8_t REG_TEST_2 = 0x7F;  //    RegTest2 Test register 0000 0000
+
+}  // namespace sx1509
+}  // namespace esphome
diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py
index 50fef7b125..3c94f4a243 100644
--- a/esphome/components/tcl112/climate.py
+++ b/esphome/components/tcl112/climate.py
@@ -1,36 +1,18 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import climate, remote_transmitter, sensor
-from esphome.const import CONF_ID, CONF_SENSOR
+from esphome.components import climate_ir
+from esphome.const import CONF_ID
 
-AUTO_LOAD = ['sensor']
+AUTO_LOAD = ['climate_ir']
 
 tcl112_ns = cg.esphome_ns.namespace('tcl112')
-Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component)
+Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR)
 
-CONF_TRANSMITTER_ID = 'transmitter_id'
-CONF_SUPPORTS_HEAT = 'supports_heat'
-CONF_SUPPORTS_COOL = 'supports_cool'
-
-CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
+CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(Tcl112Climate),
-    cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
-    cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
-    cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
-    cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
-}).extend(cv.COMPONENT_SCHEMA))
+})
 
 
 def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
-
-    cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
-    cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
-    if CONF_SENSOR in config:
-        sens = yield cg.get_variable(config[CONF_SENSOR])
-        cg.add(var.set_sensor(sens))
-
-    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
-    cg.add(var.set_transmitter(transmitter))
+    yield climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp
index cbe2f53402..2907ae1743 100644
--- a/esphome/components/tcl112/tcl112.cpp
+++ b/esphome/components/tcl112/tcl112.cpp
@@ -18,62 +18,15 @@ const uint8_t TCL112_AUTO = 8;
 const uint8_t TCL112_POWER_MASK = 0x04;
 
 const uint8_t TCL112_HALF_DEGREE = 0b00100000;
-const float TCL112_TEMP_MAX = 31.0;
-const float TCL112_TEMP_MIN = 16.0;
 
-const uint16_t TCL112_HEADER_MARK = 3000;
+const uint16_t TCL112_HEADER_MARK = 3100;
 const uint16_t TCL112_HEADER_SPACE = 1650;
 const uint16_t TCL112_BIT_MARK = 500;
-const uint16_t TCL112_ONE_SPACE = 1050;
-const uint16_t TCL112_ZERO_SPACE = 325;
+const uint16_t TCL112_ONE_SPACE = 1100;
+const uint16_t TCL112_ZERO_SPACE = 350;
 const uint32_t TCL112_GAP = TCL112_HEADER_SPACE;
 
-climate::ClimateTraits Tcl112Climate::traits() {
-  auto traits = climate::ClimateTraits();
-  traits.set_supports_current_temperature(this->sensor_ != nullptr);
-  traits.set_supports_auto_mode(true);
-  traits.set_supports_cool_mode(this->supports_cool_);
-  traits.set_supports_heat_mode(this->supports_heat_);
-  traits.set_supports_two_point_target_temperature(false);
-  traits.set_supports_away(false);
-  traits.set_visual_min_temperature(TCL112_TEMP_MIN);
-  traits.set_visual_max_temperature(TCL112_TEMP_MAX);
-  traits.set_visual_temperature_step(.5f);
-  return traits;
-}
-
-void Tcl112Climate::setup() {
-  if (this->sensor_) {
-    this->sensor_->add_on_state_callback([this](float state) {
-      this->current_temperature = state;
-      // current temperature changed, publish state
-      this->publish_state();
-    });
-    this->current_temperature = this->sensor_->state;
-  } else
-    this->current_temperature = NAN;
-  // restore set points
-  auto restore = this->restore_state_();
-  if (restore.has_value()) {
-    restore->apply(this);
-  } else {
-    // restore from defaults
-    this->mode = climate::CLIMATE_MODE_OFF;
-    this->target_temperature = 24;
-  }
-}
-
-void Tcl112Climate::control(const climate::ClimateCall &call) {
-  if (call.get_mode().has_value())
-    this->mode = *call.get_mode();
-  if (call.get_target_temperature().has_value())
-    this->target_temperature = *call.get_target_temperature();
-
-  this->transmit_state_();
-  this->publish_state();
-}
-
-void Tcl112Climate::transmit_state_() {
+void Tcl112Climate::transmit_state() {
   uint8_t remote_state[TCL112_STATE_LENGTH] = {0};
 
   // A known good state. (On, Cool, 24C)
@@ -124,7 +77,10 @@ void Tcl112Climate::transmit_state_() {
   for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++)
     remote_state[TCL112_STATE_LENGTH - 1] += remote_state[checksum_byte];
 
-  ESP_LOGV(TAG, "Sending tcl code: %u", remote_state[7]);
+  ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X   %02X %02X %02X %02X   %02X %02X %02X %02X   %02X %02X", remote_state[0],
+           remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
+           remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12],
+           remote_state[13]);
 
   auto transmit = this->transmitter_->transmit();
   auto data = transmit.get_data();
@@ -148,5 +104,76 @@ void Tcl112Climate::transmit_state_() {
   transmit.perform();
 }
 
+bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) {
+  // Validate header
+  if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) {
+    ESP_LOGV(TAG, "Header fail");
+    return false;
+  }
+
+  uint8_t remote_state[TCL112_STATE_LENGTH] = {0};
+  // Read all bytes.
+  for (int i = 0; i < TCL112_STATE_LENGTH; i++) {
+    // Read bit
+    for (int j = 0; j < 8; j++) {
+      if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE))
+        remote_state[i] |= 1 << j;
+      else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) {
+        ESP_LOGV(TAG, "Byte %d bit %d fail", i, j);
+        return false;
+      }
+    }
+  }
+  // Validate footer
+  if (!data.expect_mark(TCL112_BIT_MARK)) {
+    ESP_LOGV(TAG, "Footer fail");
+    return false;
+  }
+
+  uint8_t checksum = 0;
+  // Calculate & set the checksum for the current internal state of the remote.
+  // Stored the checksum value in the last byte.
+  for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++)
+    checksum += remote_state[checksum_byte];
+  if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) {
+    ESP_LOGV(TAG, "Checksum fail");
+    return false;
+  }
+
+  ESP_LOGV(TAG, "Received: %02X %02X %02X %02X   %02X %02X %02X %02X   %02X %02X %02X %02X   %02X %02X",
+           remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5],
+           remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11],
+           remote_state[12], remote_state[13]);
+
+  // two first bytes are constant
+  if (remote_state[0] != 0x23 || remote_state[1] != 0xCB)
+    return false;
+
+  if ((remote_state[5] & TCL112_POWER_MASK) == 0) {
+    this->mode = climate::CLIMATE_MODE_OFF;
+  } else {
+    auto mode = remote_state[6] & 0x0F;
+    switch (mode) {
+      case TCL112_HEAT:
+        this->mode = climate::CLIMATE_MODE_HEAT;
+        break;
+      case TCL112_COOL:
+        this->mode = climate::CLIMATE_MODE_COOL;
+        break;
+      case TCL112_DRY:
+      case TCL112_FAN:
+      case TCL112_AUTO:
+        this->mode = climate::CLIMATE_MODE_AUTO;
+        break;
+    }
+  }
+  auto temp = TCL112_TEMP_MAX - remote_state[7];
+  if (remote_state[12] & TCL112_HALF_DEGREE)
+    temp += .5f;
+  this->target_temperature = temp;
+  this->publish_state();
+  return true;
+}
+
 }  // namespace tcl112
 }  // namespace esphome
diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h
index 0b80dedbef..273162662d 100644
--- a/esphome/components/tcl112/tcl112.h
+++ b/esphome/components/tcl112/tcl112.h
@@ -1,39 +1,23 @@
 #pragma once
 
-#include "esphome/core/component.h"
-#include "esphome/core/automation.h"
-#include "esphome/components/climate/climate.h"
-#include "esphome/components/remote_base/remote_base.h"
-#include "esphome/components/remote_transmitter/remote_transmitter.h"
-#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/climate_ir/climate_ir.h"
 
 namespace esphome {
 namespace tcl112 {
 
-class Tcl112Climate : public climate::Climate, public Component {
+// Temperature
+const float TCL112_TEMP_MAX = 31.0;
+const float TCL112_TEMP_MIN = 16.0;
+
+class Tcl112Climate : public climate_ir::ClimateIR {
  public:
-  void setup() override;
-  void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
-    this->transmitter_ = transmitter;
-  }
-  void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
-  void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
-  void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
+  Tcl112Climate() : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {}
 
  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;
-
   /// Transmit via IR the state of this climate controller.
-  void transmit_state_();
-
-  bool supports_cool_{true};
-  bool supports_heat_{true};
-
-  remote_transmitter::RemoteTransmitterComponent *transmitter_;
-  sensor::Sensor *sensor_{nullptr};
+  void transmit_state() override;
+  /// Handle received IR Buffer
+  bool on_receive(remote_base::RemoteReceiveData data) override;
 };
 
 }  // namespace tcl112
diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py
index 808318ac81..13370d749c 100644
--- a/esphome/components/template/cover/__init__.py
+++ b/esphome/components/template/cover/__init__.py
@@ -4,7 +4,7 @@ from esphome import automation
 from esphome.components import cover
 from esphome.const import CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, CONF_ID, \
     CONF_LAMBDA, CONF_OPEN_ACTION, CONF_OPTIMISTIC, CONF_POSITION, CONF_RESTORE_MODE, \
-    CONF_STATE, CONF_STOP_ACTION
+    CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA
 from .. import template_ns
 
 TemplateCover = template_ns.class_('TemplateCover', cover.Cover, cg.Component)
@@ -24,6 +24,8 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({
     cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True),
     cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
     cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True),
+    cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True),
+    cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda,
     cv.Optional(CONF_RESTORE_MODE, default='RESTORE'): cv.enum(RESTORE_MODES, upper=True),
 }).extend(cv.COMPONENT_SCHEMA)
 
@@ -42,6 +44,14 @@ def to_code(config):
         yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION])
     if CONF_STOP_ACTION in config:
         yield automation.build_automation(var.get_stop_trigger(), [], config[CONF_STOP_ACTION])
+    if CONF_TILT_ACTION in config:
+        yield automation.build_automation(var.get_tilt_trigger(), [(float, 'tilt')],
+                                          config[CONF_TILT_ACTION])
+        cg.add(var.set_has_tilt(True))
+    if CONF_TILT_LAMBDA in config:
+        tilt_template_ = yield cg.process_lambda(config[CONF_TILT_LAMBDA], [],
+                                                 return_type=cg.optional.template(float))
+        cg.add(var.set_tilt_lambda(tilt_template_))
 
     cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
     cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
@@ -53,6 +63,7 @@ def to_code(config):
     cv.Exclusive(CONF_STATE, 'pos'): cv.templatable(cover.validate_cover_state),
     cv.Exclusive(CONF_POSITION, 'pos'): cv.templatable(cv.zero_to_one_float),
     cv.Optional(CONF_CURRENT_OPERATION): cv.templatable(cover.validate_cover_operation),
+    cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float),
 }))
 def cover_template_publish_to_code(config, action_id, template_arg, args):
     paren = yield cg.get_variable(config[CONF_ID])
@@ -63,6 +74,9 @@ def cover_template_publish_to_code(config, action_id, template_arg, args):
     if CONF_POSITION in config:
         template_ = yield cg.templatable(config[CONF_POSITION], args, float)
         cg.add(var.set_position(template_))
+    if CONF_TILT in config:
+        template_ = yield cg.templatable(config[CONF_TILT], args, float)
+        cg.add(var.set_tilt(template_))
     if CONF_CURRENT_OPERATION in config:
         template_ = yield cg.templatable(config[CONF_CURRENT_OPERATION], args, cover.CoverOperation)
         cg.add(var.set_current_operation(template_))
diff --git a/esphome/components/template/output/__init__.py b/esphome/components/template/output/__init__.py
index 5cc9e089bd..cc85a9da68 100644
--- a/esphome/components/template/output/__init__.py
+++ b/esphome/components/template/output/__init__.py
@@ -2,13 +2,12 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.components import output
-from esphome.const import CONF_ID, CONF_TYPE
+from esphome.const import CONF_ID, CONF_TYPE, CONF_BINARY
 from .. import template_ns
 
 TemplateBinaryOutput = template_ns.class_('TemplateBinaryOutput', output.BinaryOutput)
 TemplateFloatOutput = template_ns.class_('TemplateFloatOutput', output.FloatOutput)
 
-CONF_BINARY = 'binary'
 CONF_FLOAT = 'float'
 CONF_WRITE_ACTION = 'write_action'
 
diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h
index 719f0b0d62..85c2b644a0 100644
--- a/esphome/components/text_sensor/text_sensor.h
+++ b/esphome/components/text_sensor/text_sensor.h
@@ -8,12 +8,12 @@ namespace text_sensor {
 
 #define LOG_TEXT_SENSOR(prefix, type, obj) \
   if (obj != nullptr) { \
-    ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \
+    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \
     if (!obj->get_icon().empty()) { \
-      ESP_LOGCONFIG(TAG, prefix "  Icon: '%s'", obj->get_icon().c_str()); \
+      ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, obj->get_icon().c_str()); \
     } \
     if (!obj->unique_id().empty()) { \
-      ESP_LOGV(TAG, prefix "  Unique ID: '%s'", obj->unique_id().c_str()); \
+      ESP_LOGV(TAG, "%s  Unique ID: '%s'", prefix, obj->unique_id().c_str()); \
     } \
   }
 
diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py
index 634de26f00..ca1ac375ba 100644
--- a/esphome/components/time/__init__.py
+++ b/esphome/components/time/__init__.py
@@ -2,6 +2,7 @@ import bisect
 import datetime
 import logging
 import math
+import string
 
 import pytz
 import tzlocal
@@ -52,8 +53,18 @@ def _tz_dst_str(dt):
                                  _tz_timedelta(td))
 
 
-def _non_dst_tz(tz, dt):
+def _safe_tzname(tz, dt):
     tzname = tz.tzname(dt)
+    # pytz does not always return valid tznames
+    # For example: 'Europe/Saratov' returns '+04'
+    # Work around it by using a generic name for the timezone
+    if not all(c in string.ascii_letters for c in tzname):
+        return 'TZ'
+    return tzname
+
+
+def _non_dst_tz(tz, dt):
+    tzname = _safe_tzname(tz, dt)
     utcoffset = tz.utcoffset(dt)
     _LOGGER.info("Detected timezone '%s' with UTC offset %s",
                  tzname, _tz_timedelta(utcoffset))
@@ -104,8 +115,9 @@ def convert_tz(pytz_obj):
                                 _tz_dst_str(dst_begins_local), _tz_dst_str(dst_ends_local))
     _LOGGER.info("Detected timezone '%s' with UTC offset %s and daylight savings time from "
                  "%s to %s",
-                 tzname_off, _tz_timedelta(utcoffset_off), dst_begins_local.strftime("%x %X"),
-                 dst_ends_local.strftime("%x %X"))
+                 tzname_off, _tz_timedelta(utcoffset_off),
+                 dst_begins_local.strftime("%d %B %X"),
+                 dst_ends_local.strftime("%d %B %X"))
     return tzbase + tzext
 
 
diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp
index 81524826be..cb66dc3ce6 100644
--- a/esphome/components/time/real_time_clock.cpp
+++ b/esphome/components/time/real_time_clock.cpp
@@ -12,7 +12,6 @@ static const char *TAG = "time";
 
 RealTimeClock::RealTimeClock() = default;
 void RealTimeClock::call_setup() {
-  this->setup_internal_();
   setenv("TZ", this->timezone_.c_str(), 1);
   tzset();
   this->setup();
@@ -85,12 +84,12 @@ template<typename T> bool increment_time_value(T &current, uint16_t begin, uint1
 
 static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
 
-static bool days_in_month(uint8_t month, uint16_t year) {
+static uint8_t days_in_month(uint8_t month, uint16_t year) {
   static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
-  uint8_t days_in_month = DAYS_IN_MONTH[month];
+  uint8_t days = DAYS_IN_MONTH[month];
   if (month == 2 && is_leap_year(year))
-    days_in_month = 29;
-  return days_in_month;
+    return 29;
+  return days;
 }
 
 void ESPTime::increment_second() {
@@ -128,13 +127,13 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
     return;
   }
 
-  for (uint16_t i = 1970; i < this->year; i++)
+  for (int i = 1970; i < this->year; i++)
     res += is_leap_year(i) ? 366 : 365;
 
   if (use_day_of_year) {
     res += this->day_of_year - 1;
   } else {
-    for (uint8_t i = 1; i < this->month; ++i)
+    for (int i = 1; i < this->month; i++)
       res += days_in_month(i, this->year);
 
     res += this->day_of_month - 1;
diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py
index 85f606e6cc..6a7c9b6835 100644
--- a/esphome/components/time_based/cover.py
+++ b/esphome/components/time_based/cover.py
@@ -8,6 +8,8 @@ from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_ID, CONF_
 time_based_ns = cg.esphome_ns.namespace('time_based')
 TimeBasedCover = time_based_ns.class_('TimeBasedCover', cover.Cover, cg.Component)
 
+CONF_HAS_BUILT_IN_ENDSTOP = 'has_built_in_endstop'
+
 CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(TimeBasedCover),
     cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
@@ -17,6 +19,8 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({
 
     cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
     cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
+
+    cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean,
 }).extend(cv.COMPONENT_SCHEMA)
 
 
@@ -32,3 +36,5 @@ def to_code(config):
 
     cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
     yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION])
+
+    cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP]))
diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp
index bbc887debc..bdb4e5379c 100644
--- a/esphome/components/time_based/time_based_cover.cpp
+++ b/esphome/components/time_based/time_based_cover.cpp
@@ -30,13 +30,19 @@ void TimeBasedCover::loop() {
   // Recompute position every loop cycle
   this->recompute_position_();
 
-  if (this->current_operation != COVER_OPERATION_IDLE && this->is_at_target_()) {
-    this->start_direction_(COVER_OPERATION_IDLE);
+  if (this->is_at_target_()) {
+    if (this->has_built_in_endstop_ &&
+        (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
+      // Don't trigger stop, let the cover stop by itself.
+      this->current_operation = COVER_OPERATION_IDLE;
+    } else {
+      this->start_direction_(COVER_OPERATION_IDLE);
+    }
     this->publish_state();
   }
 
   // Send current position every second
-  if (this->current_operation != COVER_OPERATION_IDLE && now - this->last_publish_time_ > 1000) {
+  if (now - this->last_publish_time_ > 1000) {
     this->publish_state(false);
     this->last_publish_time_ = now;
   }
@@ -57,6 +63,12 @@ void TimeBasedCover::control(const CoverCall &call) {
     auto pos = *call.get_position();
     if (pos == this->position) {
       // already at target
+      // for covers with built in end stop, we should send the command again
+      if (this->has_built_in_endstop_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) {
+        auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
+        this->target_position_ = pos;
+        this->start_direction_(op);
+      }
     } else {
       auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
       this->target_position_ = pos;
@@ -82,7 +94,7 @@ bool TimeBasedCover::is_at_target_() const {
   }
 }
 void TimeBasedCover::start_direction_(CoverOperation dir) {
-  if (dir == this->current_operation)
+  if (dir == this->current_operation && dir != COVER_OPERATION_IDLE)
     return;
 
   this->recompute_position_();
diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h
index 60819d797b..be3a55c546 100644
--- a/esphome/components/time_based/time_based_cover.h
+++ b/esphome/components/time_based/time_based_cover.h
@@ -20,6 +20,7 @@ class TimeBasedCover : public cover::Cover, public Component {
   void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
   void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; }
   cover::CoverTraits get_traits() override;
+  void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; }
 
  protected:
   void control(const cover::CoverCall &call) override;
@@ -41,6 +42,7 @@ class TimeBasedCover : public cover::Cover, public Component {
   uint32_t start_dir_time_{0};
   uint32_t last_publish_time_{0};
   float target_position_{0};
+  bool has_built_in_endstop_{false};
 };
 
 }  // namespace time_based
diff --git a/esphome/components/tlc59208f/__init__.py b/esphome/components/tlc59208f/__init__.py
new file mode 100644
index 0000000000..4666b63b46
--- /dev/null
+++ b/esphome/components/tlc59208f/__init__.py
@@ -0,0 +1,20 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c
+from esphome.const import CONF_ID
+
+DEPENDENCIES = ['i2c']
+MULTI_CONF = True
+
+tlc59208f_ns = cg.esphome_ns.namespace('tlc59208f')
+TLC59208FOutput = tlc59208f_ns.class_('TLC59208FOutput', cg.Component, i2c.I2CDevice)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(TLC59208FOutput),
+}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
diff --git a/esphome/components/tlc59208f/output.py b/esphome/components/tlc59208f/output.py
new file mode 100644
index 0000000000..f61f7729e7
--- /dev/null
+++ b/esphome/components/tlc59208f/output.py
@@ -0,0 +1,24 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID
+from . import TLC59208FOutput, tlc59208f_ns
+
+DEPENDENCIES = ['tlc59208f']
+
+TLC59208FChannel = tlc59208f_ns.class_('TLC59208FChannel', output.FloatOutput)
+CONF_TLC59208F_ID = 'tlc59208f_id'
+
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
+    cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel),
+    cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput),
+
+    cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
+})
+
+
+def to_code(config):
+    paren = yield cg.get_variable(config[CONF_TLC59208F_ID])
+    rhs = paren.create_channel(config[CONF_CHANNEL])
+    var = cg.Pvariable(config[CONF_ID], rhs)
+    yield output.register_output(var, config)
diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp
new file mode 100644
index 0000000000..6e65ff4e76
--- /dev/null
+++ b/esphome/components/tlc59208f/tlc59208f_output.cpp
@@ -0,0 +1,155 @@
+#include "tlc59208f_output.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace tlc59208f {
+
+static const char *TAG = "tlc59208f";
+
+// * marks register defaults
+// 0*: Register auto increment disabled, 1: Register auto increment enabled
+const uint8_t TLC59208F_MODE1_AI2 = (1 << 7);
+// 0*: don't auto increment bit 1, 1: auto increment bit 1
+const uint8_t TLC59208F_MODE1_AI1 = (1 << 6);
+// 0*: don't auto increment bit 0, 1: auto increment bit 0
+const uint8_t TLC59208F_MODE1_AI0 = (1 << 5);
+// 0: normal mode, 1*: low power mode, osc off
+const uint8_t TLC59208F_MODE1_SLEEP = (1 << 4);
+// 0*: device doesn't respond to i2c bus sub-address 1, 1: responds
+const uint8_t TLC59208F_MODE1_SUB1 = (1 << 3);
+// 0*: device doesn't respond to i2c bus sub-address 2, 1: responds
+const uint8_t TLC59208F_MODE1_SUB2 = (1 << 2);
+// 0*: device doesn't respond to i2c bus sub-address 3, 1: responds
+const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1);
+// 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call
+const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0);
+
+// 0*: Group dimming, 1: Group blinking
+const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5);
+// 0*: Output change on Stop command, 1: Output change on ACK
+const uint8_t TLC59208F_MODE2_OCH = (1 << 3);
+// 0*: WDT disabled, 1: WDT enabled
+const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2);
+// WDT timeouts
+const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0);
+const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0);
+const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0);
+const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0);
+
+// --- Special function ---
+// Call address to perform software reset, no devices will ACK
+const uint8_t TLC59208F_SWRST_ADDR = 0x96;  //(0x4b 7-bit addr + ~W)
+const uint8_t TLC59208F_SWRST_SEQ[2] = {0xa5, 0x5a};
+
+// --- Registers ---2
+// Mode register 1
+const uint8_t TLC59208F_REG_MODE1 = 0x00;
+// Mode register 2
+const uint8_t TLC59208F_REG_MODE2 = 0x01;
+// PWM0
+const uint8_t TLC59208F_REG_PWM0 = 0x02;
+// Group PWM
+const uint8_t TLC59208F_REG_GROUPPWM = 0x0a;
+// Group Freq
+const uint8_t TLC59208F_REG_GROUPFREQ = 0x0b;
+// LEDOUTx registers
+const uint8_t TLC59208F_REG_LEDOUT0 = 0x0c;
+const uint8_t TLC59208F_REG_LEDOUT1 = 0x0d;
+// Sub-address registers
+const uint8_t TLC59208F_REG_SUBADR1 = 0x0e;  // default: 0x92 (8-bit addr)
+const uint8_t TLC59208F_REG_SUBADR2 = 0x0f;  // default: 0x94 (8-bit addr)
+const uint8_t TLC59208F_REG_SUBADR3 = 0x10;  // default: 0x98 (8-bit addr)
+// All call address register
+const uint8_t TLC59208F_REG_ALLCALLADR = 0x11;  // default: 0xd0 (8-bit addr)
+
+// --- Output modes ---
+static const uint8_t LDR_OFF = 0x00;
+static const uint8_t LDR_ON = 0x01;
+static const uint8_t LDR_PWM = 0x02;
+static const uint8_t LDR_GRPPWM = 0x03;
+
+void TLC59208FOutput::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up TLC59208FOutputComponent...");
+
+  ESP_LOGV(TAG, "  Resetting all devices on the bus...");
+
+  // Reset all devices on the bus
+  if (!this->parent_->write_byte(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ[0], TLC59208F_SWRST_SEQ[1])) {
+    ESP_LOGE(TAG, "RESET failed");
+    this->mark_failed();
+    return;
+  }
+
+  // Auto increment registers, and respond to all-call address
+  if (!this->write_byte(TLC59208F_REG_MODE1, TLC59208F_MODE1_AI2 | TLC59208F_MODE1_ALLCALL)) {
+    ESP_LOGE(TAG, "MODE1 failed");
+    this->mark_failed();
+    return;
+  }
+  if (!this->write_byte(TLC59208F_REG_MODE2, this->mode_)) {
+    ESP_LOGE(TAG, "MODE2 failed");
+    this->mark_failed();
+    return;
+  }
+  // Set all 3 outputs to be individually controlled
+  // TODO: think of a way to support group dimming
+  if (!this->write_byte(TLC59208F_REG_LEDOUT0, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) {
+    ESP_LOGE(TAG, "LEDOUT0 failed");
+    this->mark_failed();
+    return;
+  }
+  if (!this->write_byte(TLC59208F_REG_LEDOUT1, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) {
+    ESP_LOGE(TAG, "LEDOUT1 failed");
+    this->mark_failed();
+    return;
+  }
+  delayMicroseconds(500);
+
+  this->loop();
+}
+
+void TLC59208FOutput::dump_config() {
+  ESP_LOGCONFIG(TAG, "TLC59208F:");
+  ESP_LOGCONFIG(TAG, "  Mode: 0x%02X", this->mode_);
+
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Setting up TLC59208F failed!");
+  }
+}
+
+void TLC59208FOutput::loop() {
+  if (this->min_channel_ == 0xFF || !this->update_)
+    return;
+
+  for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) {
+    uint8_t pwm = this->pwm_amounts_[channel];
+    ESP_LOGVV(TAG, "Channel %02u: pwm=%04u ", channel, pwm);
+
+    uint8_t reg = TLC59208F_REG_PWM0 + channel;
+    if (!this->write_byte(reg, pwm)) {
+      this->status_set_warning();
+      return;
+    }
+  }
+
+  this->status_clear_warning();
+  this->update_ = false;
+}
+
+TLC59208FChannel *TLC59208FOutput::create_channel(uint8_t channel) {
+  this->min_channel_ = std::min(this->min_channel_, channel);
+  this->max_channel_ = std::max(this->max_channel_, channel);
+  auto *c = new TLC59208FChannel(this, channel);
+  return c;
+}
+
+void TLC59208FChannel::write_state(float state) {
+  const uint8_t max_duty = 255;
+  const float duty_rounded = roundf(state * max_duty);
+  auto duty = static_cast<uint8_t>(duty_rounded);
+  this->parent_->set_channel_value_(this->channel_, duty);
+}
+
+}  // namespace tlc59208f
+}  // namespace esphome
diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h
new file mode 100644
index 0000000000..06b7adc882
--- /dev/null
+++ b/esphome/components/tlc59208f/tlc59208f_output.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/output/float_output.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace tlc59208f {
+
+// 0*: Group dimming, 1: Group blinking
+extern const uint8_t TLC59208F_MODE2_DMBLNK;
+// 0*: Output change on Stop command, 1: Output change on ACK
+extern const uint8_t TLC59208F_MODE2_OCH;
+// 0*: WDT disabled, 1: WDT enabled
+extern const uint8_t TLC59208F_MODE2_WDTEN;
+// WDT timeouts
+extern const uint8_t TLC59208F_MODE2_WDT_5MS;
+extern const uint8_t TLC59208F_MODE2_WDT_15MS;
+extern const uint8_t TLC59208F_MODE2_WDT_25MS;
+extern const uint8_t TLC59208F_MODE2_WDT_35MS;
+
+class TLC59208FOutput;
+
+class TLC59208FChannel : public output::FloatOutput {
+ public:
+  TLC59208FChannel(TLC59208FOutput *parent, uint8_t channel) : parent_(parent), channel_(channel) {}
+
+ protected:
+  void write_state(float state) override;
+
+  TLC59208FOutput *parent_;
+  uint8_t channel_;
+};
+
+/// TLC59208F float output component.
+class TLC59208FOutput : public Component, public i2c::I2CDevice {
+ public:
+  TLC59208FOutput(uint8_t mode = TLC59208F_MODE2_OCH) : mode_(mode) {}
+
+  TLC59208FChannel *create_channel(uint8_t channel);
+
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+  void loop() override;
+
+ protected:
+  friend TLC59208FChannel;
+
+  void set_channel_value_(uint8_t channel, uint8_t value) {
+    if (this->pwm_amounts_[channel] != value)
+      this->update_ = true;
+    this->pwm_amounts_[channel] = value;
+  }
+
+  uint8_t mode_;
+
+  uint8_t min_channel_{0xFF};
+  uint8_t max_channel_{0x00};
+  uint8_t pwm_amounts_[256] = {
+      0,
+  };
+  bool update_{true};
+};
+
+}  // namespace tlc59208f
+}  // namespace esphome
diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py
new file mode 100644
index 0000000000..541f10f862
--- /dev/null
+++ b/esphome/components/tuya/__init__.py
@@ -0,0 +1,20 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import uart
+from esphome.const import CONF_ID
+
+DEPENDENCIES = ['uart']
+
+tuya_ns = cg.esphome_ns.namespace('tuya')
+Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice)
+
+CONF_TUYA_ID = 'tuya_id'
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(Tuya),
+}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield uart.register_uart_device(var, config)
diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py
new file mode 100644
index 0000000000..605bdae32e
--- /dev/null
+++ b/esphome/components/tuya/light/__init__.py
@@ -0,0 +1,44 @@
+from esphome.components import light
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \
+    CONF_DEFAULT_TRANSITION_LENGTH
+from .. import tuya_ns, CONF_TUYA_ID, Tuya
+
+DEPENDENCIES = ['tuya']
+
+CONF_DIMMER_DATAPOINT = "dimmer_datapoint"
+CONF_SWITCH_DATAPOINT = "switch_datapoint"
+
+TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component)
+
+CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({
+    cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight),
+    cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
+    cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t,
+    cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
+    cv.Optional(CONF_MIN_VALUE): cv.int_,
+    cv.Optional(CONF_MAX_VALUE): cv.int_,
+
+    # Change the default gamma_correct and default transition length settings.
+    # The Tuya MCU handles transitions and gamma correction on its own.
+    cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
+    cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds,
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
+    yield cg.register_component(var, config)
+    yield light.register_light(var, config)
+
+    if CONF_DIMMER_DATAPOINT in config:
+        cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT]))
+    if CONF_SWITCH_DATAPOINT in config:
+        cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
+    if CONF_MIN_VALUE in config:
+        cg.add(var.set_min_value(config[CONF_MIN_VALUE]))
+    if CONF_MAX_VALUE in config:
+        cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
+    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    cg.add(var.set_tuya_parent(paren))
diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp
new file mode 100644
index 0000000000..9696252049
--- /dev/null
+++ b/esphome/components/tuya/light/tuya_light.cpp
@@ -0,0 +1,85 @@
+#include "esphome/core/log.h"
+#include "tuya_light.h"
+
+namespace esphome {
+namespace tuya {
+
+static const char *TAG = "tuya.light";
+
+void TuyaLight::setup() {
+  if (this->dimmer_id_.has_value()) {
+    this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) {
+      auto call = this->state_->make_call();
+      call.set_brightness(float(datapoint.value_uint) / this->max_value_);
+      call.perform();
+    });
+  }
+  if (switch_id_.has_value()) {
+    this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
+      auto call = this->state_->make_call();
+      call.set_state(datapoint.value_bool);
+      call.perform();
+    });
+  }
+}
+
+void TuyaLight::dump_config() {
+  ESP_LOGCONFIG(TAG, "Tuya Dimmer:");
+  if (this->dimmer_id_.has_value())
+    ESP_LOGCONFIG(TAG, "   Dimmer has datapoint ID %u", *this->dimmer_id_);
+  if (this->switch_id_.has_value())
+    ESP_LOGCONFIG(TAG, "   Switch has datapoint ID %u", *this->switch_id_);
+}
+
+light::LightTraits TuyaLight::get_traits() {
+  auto traits = light::LightTraits();
+  traits.set_supports_brightness(this->dimmer_id_.has_value());
+  return traits;
+}
+
+void TuyaLight::setup_state(light::LightState *state) { state_ = state; }
+
+void TuyaLight::write_state(light::LightState *state) {
+  float brightness;
+  state->current_values_as_brightness(&brightness);
+
+  if (brightness == 0.0f) {
+    // turning off, first try via switch (if exists), then dimmer
+    if (switch_id_.has_value()) {
+      TuyaDatapoint datapoint{};
+      datapoint.id = *this->switch_id_;
+      datapoint.type = TuyaDatapointType::BOOLEAN;
+      datapoint.value_bool = false;
+
+      parent_->set_datapoint_value(datapoint);
+    } else if (dimmer_id_.has_value()) {
+      TuyaDatapoint datapoint{};
+      datapoint.id = *this->dimmer_id_;
+      datapoint.type = TuyaDatapointType::INTEGER;
+      datapoint.value_int = 0;
+      parent_->set_datapoint_value(datapoint);
+    }
+    return;
+  }
+
+  auto brightness_int = static_cast<uint32_t>(brightness * this->max_value_);
+  brightness_int = std::max(brightness_int, this->min_value_);
+
+  if (this->dimmer_id_.has_value()) {
+    TuyaDatapoint datapoint{};
+    datapoint.id = *this->dimmer_id_;
+    datapoint.type = TuyaDatapointType::INTEGER;
+    datapoint.value_int = brightness_int;
+    parent_->set_datapoint_value(datapoint);
+  }
+  if (this->switch_id_.has_value()) {
+    TuyaDatapoint datapoint{};
+    datapoint.id = *this->switch_id_;
+    datapoint.type = TuyaDatapointType::BOOLEAN;
+    datapoint.value_bool = true;
+    parent_->set_datapoint_value(datapoint);
+  }
+}
+
+}  // namespace tuya
+}  // namespace esphome
diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h
new file mode 100644
index 0000000000..581512c29c
--- /dev/null
+++ b/esphome/components/tuya/light/tuya_light.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/tuya/tuya.h"
+#include "esphome/components/light/light_output.h"
+
+namespace esphome {
+namespace tuya {
+
+class TuyaLight : public Component, public light::LightOutput {
+ public:
+  void setup() override;
+  void dump_config() override;
+  void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; }
+  void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
+  void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
+  void set_min_value(uint32_t min_value) { min_value_ = min_value; }
+  void set_max_value(uint32_t max_value) { max_value_ = max_value; }
+  light::LightTraits get_traits() override;
+  void setup_state(light::LightState *state) override;
+  void write_state(light::LightState *state) override;
+
+ protected:
+  void update_dimmer_(uint32_t value);
+  void update_switch_(uint32_t value);
+
+  Tuya *parent_;
+  optional<uint8_t> dimmer_id_{};
+  optional<uint8_t> switch_id_{};
+  uint32_t min_value_ = 0;
+  uint32_t max_value_ = 255;
+  light::LightState *state_{nullptr};
+};
+
+}  // namespace tuya
+}  // namespace esphome
diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp
new file mode 100644
index 0000000000..cb796644c8
--- /dev/null
+++ b/esphome/components/tuya/tuya.cpp
@@ -0,0 +1,298 @@
+#include "tuya.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace tuya {
+
+static const char *TAG = "tuya";
+
+void Tuya::setup() {
+  this->send_empty_command_(TuyaCommandType::MCU_CONF);
+  this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
+}
+
+void Tuya::loop() {
+  while (this->available()) {
+    uint8_t c;
+    this->read_byte(&c);
+    this->handle_char_(c);
+  }
+}
+
+void Tuya::dump_config() {
+  ESP_LOGCONFIG(TAG, "Tuya:");
+  if ((gpio_status_ != -1) || (gpio_reset_ != -1))
+    ESP_LOGCONFIG(TAG, "  GPIO MCU configuration not supported!");
+  for (auto &info : this->datapoints_) {
+    if (info.type == TuyaDatapointType::BOOLEAN)
+      ESP_LOGCONFIG(TAG, "  Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
+    else if (info.type == TuyaDatapointType::INTEGER)
+      ESP_LOGCONFIG(TAG, "  Datapoint %d: int value (value: %d)", info.id, info.value_int);
+    else if (info.type == TuyaDatapointType::ENUM)
+      ESP_LOGCONFIG(TAG, "  Datapoint %d: enum (value: %d)", info.id, info.value_enum);
+    else if (info.type == TuyaDatapointType::BITMASK)
+      ESP_LOGCONFIG(TAG, "  Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask);
+    else
+      ESP_LOGCONFIG(TAG, "  Datapoint %d: unknown", info.id);
+  }
+  if (this->datapoints_.empty()) {
+    ESP_LOGCONFIG(TAG, "  Received no datapoints! Please make sure this is a supported Tuya device.");
+  }
+  this->check_uart_settings(9600);
+}
+
+bool Tuya::validate_message_() {
+  uint32_t at = this->rx_message_.size() - 1;
+  auto *data = &this->rx_message_[0];
+  uint8_t new_byte = data[at];
+
+  // Byte 0: HEADER1 (always 0x55)
+  if (at == 0)
+    return new_byte == 0x55;
+  // Byte 1: HEADER2 (always 0xAA)
+  if (at == 1)
+    return new_byte == 0xAA;
+
+  // Byte 2: VERSION
+  // no validation for the following fields:
+  uint8_t version = data[2];
+  if (at == 2)
+    return true;
+  // Byte 3: COMMAND
+  uint8_t command = data[3];
+  if (at == 3)
+    return true;
+
+  // Byte 4: LENGTH1
+  // Byte 5: LENGTH2
+  if (at <= 5)
+    // no validation for these fields
+    return true;
+
+  uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5]));
+
+  // wait until all data is read
+  if (at - 6 < length)
+    return true;
+
+  // Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256
+  uint8_t rx_checksum = new_byte;
+  uint8_t calc_checksum = 0;
+  for (uint32_t i = 0; i < 6 + length; i++)
+    calc_checksum += data[i];
+
+  if (rx_checksum != calc_checksum) {
+    ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum);
+    return false;
+  }
+
+  // valid message
+  const uint8_t *message_data = data + 6;
+  ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version,
+           hexencode(message_data, length).c_str());
+  this->handle_command_(command, version, message_data, length);
+
+  // return false to reset rx buffer
+  return false;
+}
+
+void Tuya::handle_char_(uint8_t c) {
+  this->rx_message_.push_back(c);
+  if (!this->validate_message_()) {
+    this->rx_message_.clear();
+  }
+}
+
+void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) {
+  uint8_t c;
+  switch ((TuyaCommandType) command) {
+    case TuyaCommandType::HEARTBEAT:
+      ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
+      if (buffer[0] == 0) {
+        ESP_LOGI(TAG, "MCU restarted");
+        this->send_empty_command_(TuyaCommandType::QUERY_STATE);
+      }
+      break;
+    case TuyaCommandType::QUERY_PRODUCT: {
+      // check it is a valid string
+      bool valid = false;
+      for (int i = 0; i < len; i++) {
+        if (buffer[i] == 0x00) {
+          valid = true;
+          break;
+        }
+      }
+      if (valid) {
+        ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast<const char *>(buffer));
+      }
+      break;
+    }
+    case TuyaCommandType::MCU_CONF:
+      if (len >= 2) {
+        gpio_status_ = buffer[0];
+        gpio_reset_ = buffer[1];
+      }
+      // set wifi state LED to off or on depending on the MCU firmware
+      // but it shouldn't be blinking
+      c = 0x3;
+      this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1);
+      this->send_empty_command_(TuyaCommandType::QUERY_STATE);
+      break;
+    case TuyaCommandType::WIFI_STATE:
+      break;
+    case TuyaCommandType::WIFI_RESET:
+      ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled");
+      break;
+    case TuyaCommandType::WIFI_SELECT:
+      ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
+      break;
+    case TuyaCommandType::SET_DATAPOINT:
+      break;
+    case TuyaCommandType::STATE: {
+      this->handle_datapoint_(buffer, len);
+      break;
+    }
+    case TuyaCommandType::QUERY_STATE:
+      break;
+    default:
+      ESP_LOGE(TAG, "invalid command (%02x) received", command);
+  }
+}
+
+void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
+  if (len < 2)
+    return;
+
+  TuyaDatapoint datapoint{};
+  datapoint.id = buffer[0];
+  datapoint.type = (TuyaDatapointType) buffer[1];
+  datapoint.value_uint = 0;
+
+  size_t data_size = (buffer[2] << 8) + buffer[3];
+  const uint8_t *data = buffer + 4;
+  size_t data_len = len - 4;
+  if (data_size != data_len) {
+    ESP_LOGW(TAG, "invalid datapoint update");
+    return;
+  }
+
+  switch (datapoint.type) {
+    case TuyaDatapointType::BOOLEAN:
+      if (data_len != 1)
+        return;
+      datapoint.value_bool = data[0];
+      break;
+    case TuyaDatapointType::INTEGER:
+      if (data_len != 4)
+        return;
+      datapoint.value_uint =
+          (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0);
+      break;
+    case TuyaDatapointType::ENUM:
+      if (data_len != 1)
+        return;
+      datapoint.value_enum = data[0];
+      break;
+    case TuyaDatapointType::BITMASK:
+      if (data_len != 2)
+        return;
+      datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0);
+      break;
+    default:
+      return;
+  }
+  ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint);
+
+  // Update internal datapoints
+  bool found = false;
+  for (auto &other : this->datapoints_) {
+    if (other.id == datapoint.id) {
+      other = datapoint;
+      found = true;
+    }
+  }
+  if (!found) {
+    this->datapoints_.push_back(datapoint);
+    // New datapoint found, reprint dump_config after a delay.
+    this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); });
+  }
+
+  // Run through listeners
+  for (auto &listener : this->listeners_)
+    if (listener.datapoint_id == datapoint.id)
+      listener.on_datapoint(datapoint);
+}
+
+void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) {
+  uint8_t len_hi = len >> 8;
+  uint8_t len_lo = len >> 0;
+  this->write_array({0x55, 0xAA,
+                     0x00,  // version
+                     (uint8_t) command, len_hi, len_lo});
+  if (len != 0)
+    this->write_array(buffer, len);
+
+  uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo;
+  for (int i = 0; i < len; i++)
+    checksum += buffer[i];
+  this->write_byte(checksum);
+}
+
+void Tuya::set_datapoint_value(TuyaDatapoint datapoint) {
+  std::vector<uint8_t> buffer;
+  ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint);
+  for (auto &other : this->datapoints_) {
+    if (other.id == datapoint.id) {
+      if (other.value_uint == datapoint.value_uint) {
+        ESP_LOGV(TAG, "Not sending unchanged value");
+        return;
+      }
+    }
+  }
+  buffer.push_back(datapoint.id);
+  buffer.push_back(static_cast<uint8_t>(datapoint.type));
+
+  std::vector<uint8_t> data;
+  switch (datapoint.type) {
+    case TuyaDatapointType::BOOLEAN:
+      data.push_back(datapoint.value_bool);
+      break;
+    case TuyaDatapointType::INTEGER:
+      data.push_back(datapoint.value_uint >> 24);
+      data.push_back(datapoint.value_uint >> 16);
+      data.push_back(datapoint.value_uint >> 8);
+      data.push_back(datapoint.value_uint >> 0);
+      break;
+    case TuyaDatapointType::ENUM:
+      data.push_back(datapoint.value_enum);
+      break;
+    case TuyaDatapointType::BITMASK:
+      data.push_back(datapoint.value_bitmask >> 8);
+      data.push_back(datapoint.value_bitmask >> 0);
+      break;
+    default:
+      return;
+  }
+
+  buffer.push_back(data.size() >> 8);
+  buffer.push_back(data.size() >> 0);
+  buffer.insert(buffer.end(), data.begin(), data.end());
+  this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size());
+}
+
+void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) {
+  auto listener = TuyaDatapointListener{
+      .datapoint_id = datapoint_id,
+      .on_datapoint = func,
+  };
+  this->listeners_.push_back(listener);
+
+  // Run through existing datapoints
+  for (auto &datapoint : this->datapoints_)
+    if (datapoint.id == datapoint_id)
+      func(datapoint);
+}
+
+}  // namespace tuya
+}  // namespace esphome
diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h
new file mode 100644
index 0000000000..6bc6d92da0
--- /dev/null
+++ b/esphome/components/tuya/tuya.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/uart/uart.h"
+
+namespace esphome {
+namespace tuya {
+
+enum class TuyaDatapointType : uint8_t {
+  RAW = 0x00,      // variable length
+  BOOLEAN = 0x01,  // 1 byte (0/1)
+  INTEGER = 0x02,  // 4 byte
+  STRING = 0x03,   // variable length
+  ENUM = 0x04,     // 1 byte
+  BITMASK = 0x05,  // 2 bytes
+};
+
+struct TuyaDatapoint {
+  uint8_t id;
+  TuyaDatapointType type;
+  union {
+    bool value_bool;
+    int value_int;
+    uint32_t value_uint;
+    uint8_t value_enum;
+    uint16_t value_bitmask;
+  };
+};
+
+struct TuyaDatapointListener {
+  uint8_t datapoint_id;
+  std::function<void(TuyaDatapoint)> on_datapoint;
+};
+
+enum class TuyaCommandType : uint8_t {
+  HEARTBEAT = 0x00,
+  QUERY_PRODUCT = 0x01,
+  MCU_CONF = 0x02,
+  WIFI_STATE = 0x03,
+  WIFI_RESET = 0x04,
+  WIFI_SELECT = 0x05,
+  SET_DATAPOINT = 0x06,
+  STATE = 0x07,
+  QUERY_STATE = 0x08,
+};
+
+class Tuya : public Component, public uart::UARTDevice {
+ public:
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+  void setup() override;
+  void loop() override;
+  void dump_config() override;
+  void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
+  void set_datapoint_value(TuyaDatapoint datapoint);
+
+ protected:
+  void handle_char_(uint8_t c);
+  void handle_datapoint_(const uint8_t *buffer, size_t len);
+  bool validate_message_();
+
+  void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len);
+  void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len);
+  void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); }
+
+  int gpio_status_ = -1;
+  int gpio_reset_ = -1;
+  std::vector<TuyaDatapointListener> listeners_;
+  std::vector<TuyaDatapoint> datapoints_;
+  std::vector<uint8_t> rx_message_;
+};
+
+}  // namespace tuya
+}  // namespace esphome
diff --git a/esphome/components/tx20/__init__.py b/esphome/components/tx20/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py
new file mode 100644
index 0000000000..daa6677196
--- /dev/null
+++ b/esphome/components/tx20/sensor.py
@@ -0,0 +1,38 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import sensor
+from esphome.const import CONF_ID, CONF_WIND_SPEED, CONF_PIN, \
+    CONF_WIND_DIRECTION_DEGREES, UNIT_KILOMETER_PER_HOUR, \
+    UNIT_EMPTY, ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION
+
+tx20_ns = cg.esphome_ns.namespace('tx20')
+Tx20Component = tx20_ns.class_('Tx20Component', cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(Tx20Component),
+    cv.Optional(CONF_WIND_SPEED):
+        sensor.sensor_schema(UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, 1),
+    cv.Optional(CONF_WIND_DIRECTION_DEGREES):
+        sensor.sensor_schema(UNIT_EMPTY, ICON_SIGN_DIRECTION, 1),
+    cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema,
+                                  pins.validate_has_interrupt),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+
+    if CONF_WIND_SPEED in config:
+        conf = config[CONF_WIND_SPEED]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_wind_speed_sensor(sens))
+
+    if CONF_WIND_DIRECTION_DEGREES in config:
+        conf = config[CONF_WIND_DIRECTION_DEGREES]
+        sens = yield sensor.new_sensor(conf)
+        cg.add(var.set_wind_direction_degrees_sensor(sens))
+
+    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    cg.add(var.set_pin(pin))
diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp
new file mode 100644
index 0000000000..f3dafda288
--- /dev/null
+++ b/esphome/components/tx20/tx20.cpp
@@ -0,0 +1,195 @@
+#include "tx20.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace tx20 {
+
+static const char *TAG = "tx20";
+static const uint8_t MAX_BUFFER_SIZE = 41;
+static const uint16_t TX20_MAX_TIME = MAX_BUFFER_SIZE * 1200 + 5000;
+static const uint16_t TX20_BIT_TIME = 1200;
+static const char *DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
+                                   "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"};
+
+void Tx20Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up Tx20");
+  this->pin_->setup();
+
+  this->store_.buffer = new uint16_t[MAX_BUFFER_SIZE];
+  this->store_.pin = this->pin_->to_isr();
+  this->store_.reset();
+
+  this->pin_->attach_interrupt(Tx20ComponentStore::gpio_intr, &this->store_, CHANGE);
+}
+void Tx20Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "Tx20:");
+
+  LOG_SENSOR("  ", "Wind speed:", this->wind_speed_sensor_);
+  LOG_SENSOR("  ", "Wind direction degrees:", this->wind_direction_degrees_sensor_);
+
+  LOG_PIN("  Pin: ", this->pin_);
+}
+void Tx20Component::loop() {
+  if (this->store_.tx20_available) {
+    this->decode_and_publish_();
+    this->store_.reset();
+  }
+}
+
+float Tx20Component::get_setup_priority() const { return setup_priority::DATA; }
+
+std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; }
+
+void Tx20Component::decode_and_publish_() {
+  ESP_LOGVV(TAG, "Decode Tx20...");
+
+  std::string string_buffer;
+  std::string string_buffer_2;
+  std::vector<bool> bit_buffer;
+  bool current_bit = true;
+
+  for (int i = 1; i <= this->store_.buffer_index; i++) {
+    string_buffer_2 += to_string(this->store_.buffer[i]) + ", ";
+    uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME;
+    // ignore segments at the end that were too short
+    string_buffer.append(repeat, current_bit ? '1' : '0');
+    bit_buffer.insert(bit_buffer.end(), repeat, current_bit);
+    current_bit = !current_bit;
+  }
+  current_bit = !current_bit;
+  if (string_buffer.length() < MAX_BUFFER_SIZE) {
+    uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length();
+    string_buffer_2 += to_string(remain) + ", ";
+    string_buffer.append(remain, current_bit ? '1' : '0');
+    bit_buffer.insert(bit_buffer.end(), remain, current_bit);
+  }
+
+  uint8_t tx20_sa = 0;
+  uint8_t tx20_sb = 0;
+  uint8_t tx20_sd = 0;
+  uint8_t tx20_se = 0;
+  uint16_t tx20_sc = 0;
+  uint16_t tx20_sf = 0;
+  uint8_t tx20_wind_direction = 0;
+  float tx20_wind_speed_kmh = 0;
+  uint8_t bit_count = 0;
+
+  for (int i = 41; i > 0; i--) {
+    uint8_t bit = bit_buffer.at(bit_count);
+    bit_count++;
+    if (i > 41 - 5) {
+      // start, inverted
+      tx20_sa = (tx20_sa << 1) | (bit ^ 1);
+    } else if (i > 41 - 5 - 4) {
+      // wind dir, inverted
+      tx20_sb = tx20_sb >> 1 | ((bit ^ 1) << 3);
+    } else if (i > 41 - 5 - 4 - 12) {
+      // windspeed, inverted
+      tx20_sc = tx20_sc >> 1 | ((bit ^ 1) << 11);
+    } else if (i > 41 - 5 - 4 - 12 - 4) {
+      // checksum, inverted
+      tx20_sd = tx20_sd >> 1 | ((bit ^ 1) << 3);
+    } else if (i > 41 - 5 - 4 - 12 - 4 - 4) {
+      // wind dir
+      tx20_se = tx20_se >> 1 | (bit << 3);
+    } else {
+      // windspeed
+      tx20_sf = tx20_sf >> 1 | (bit << 11);
+    }
+  }
+
+  uint8_t chk = (tx20_sb + (tx20_sc & 0xf) + ((tx20_sc >> 4) & 0xf) + ((tx20_sc >> 8) & 0xf));
+  chk &= 0xf;
+  bool value_set = false;
+  // checks:
+  // 1. Check that the start frame is 00100 (0x04)
+  // 2. Check received checksum matches calculated checksum
+  // 3. Check that Wind Direction matches Wind Direction (Inverted)
+  // 4. Check that Wind Speed matches Wind Speed (Inverted)
+  ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str());
+  ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str());
+
+  if (tx20_sa == 4) {
+    if (chk == tx20_sd) {
+      if (tx20_sf == tx20_sc) {
+        tx20_wind_speed_kmh = float(tx20_sc) * 0.36;
+        ESP_LOGV(TAG, "WindSpeed %f", tx20_wind_speed_kmh);
+        if (this->wind_speed_sensor_ != nullptr)
+          this->wind_speed_sensor_->publish_state(tx20_wind_speed_kmh);
+        value_set = true;
+      }
+      if (tx20_se == tx20_sb) {
+        tx20_wind_direction = tx20_se;
+        if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) {
+          wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction];
+        }
+        ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction);
+        if (this->wind_direction_degrees_sensor_ != nullptr)
+          this->wind_direction_degrees_sensor_->publish_state(float(tx20_wind_direction) * 22.5f);
+        value_set = true;
+      }
+      if (!value_set) {
+        ESP_LOGW(TAG, "No value set!");
+      }
+    } else {
+      ESP_LOGW(TAG, "Checksum wrong!");
+    }
+  } else {
+    ESP_LOGW(TAG, "Start wrong!");
+  }
+}
+
+void ICACHE_RAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
+  arg->pin_state = arg->pin->digital_read();
+  const uint32_t now = micros();
+  if (!arg->start_time) {
+    // only detect a start if the bit is high
+    if (!arg->pin_state) {
+      return;
+    }
+    arg->buffer[arg->buffer_index] = 1;
+    arg->start_time = now;
+    arg->buffer_index++;
+    return;
+  }
+  const uint32_t delay = now - arg->start_time;
+  const uint8_t index = arg->buffer_index;
+
+  // first delay has to be ~2400
+  if (index == 1 && (delay > 3000 || delay < 2400)) {
+    arg->reset();
+    return;
+  }
+  // second delay has to be ~1200
+  if (index == 2 && (delay > 1500 || delay < 1200)) {
+    arg->reset();
+    return;
+  }
+  // third delay has to be ~2400
+  if (index == 3 && (delay > 3000 || delay < 2400)) {
+    arg->reset();
+    return;
+  }
+
+  if (arg->tx20_available || ((arg->spent_time + delay > TX20_MAX_TIME) && arg->start_time)) {
+    arg->tx20_available = true;
+    return;
+  }
+  if (index <= MAX_BUFFER_SIZE) {
+    arg->buffer[index] = delay;
+  }
+  arg->spent_time += delay;
+  arg->start_time = now;
+  arg->buffer_index++;
+}
+void ICACHE_RAM_ATTR Tx20ComponentStore::reset() {
+  tx20_available = false;
+  buffer_index = 0;
+  spent_time = 0;
+  // rearm it!
+  start_time = 0;
+}
+
+}  // namespace tx20
+}  // namespace esphome
diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h
new file mode 100644
index 0000000000..8b79deffbc
--- /dev/null
+++ b/esphome/components/tx20/tx20.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace tx20 {
+
+/// Store data in a class that doesn't use multiple-inheritance (vtables in flash)
+struct Tx20ComponentStore {
+  volatile uint16_t *buffer;
+  volatile uint32_t start_time;
+  volatile uint8_t buffer_index;
+  volatile uint32_t spent_time;
+  volatile bool tx20_available;
+  volatile bool pin_state;
+  ISRInternalGPIOPin *pin;
+
+  void reset();
+  static void gpio_intr(Tx20ComponentStore *arg);
+};
+
+/// This class implements support for the Tx20 Wind sensor.
+class Tx20Component : public Component {
+ public:
+  /// Get the textual representation of the wind direction ('N', 'SSE', ..).
+  std::string get_wind_cardinal_direction() const;
+
+  void set_pin(GPIOPin *pin) { pin_ = pin; }
+  void set_wind_speed_sensor(sensor::Sensor *wind_speed_sensor) { wind_speed_sensor_ = wind_speed_sensor; }
+  void set_wind_direction_degrees_sensor(sensor::Sensor *wind_direction_degrees_sensor) {
+    wind_direction_degrees_sensor_ = wind_direction_degrees_sensor;
+  }
+
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void loop() override;
+
+ protected:
+  void decode_and_publish_();
+
+  std::string wind_cardinal_direction_;
+  GPIOPin *pin_;
+  sensor::Sensor *wind_speed_sensor_;
+  sensor::Sensor *wind_direction_degrees_sensor_;
+  Tx20ComponentStore store_;
+};
+
+}  // namespace tx20
+}  // namespace esphome
diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py
index c374568149..2511cf28b1 100644
--- a/esphome/components/uart/__init__.py
+++ b/esphome/components/uart/__init__.py
@@ -29,15 +29,18 @@ def validate_rx_pin(value):
     return value
 
 
+CONF_STOP_BITS = 'stop_bits'
 CONFIG_SCHEMA = cv.All(cv.Schema({
     cv.GenerateID(): cv.declare_id(UARTComponent),
-    cv.Required(CONF_BAUD_RATE): cv.int_range(min=1, max=115200),
+    cv.Required(CONF_BAUD_RATE): cv.int_range(min=1),
     cv.Optional(CONF_TX_PIN): pins.output_pin,
     cv.Optional(CONF_RX_PIN): validate_rx_pin,
+    cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True),
 }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN))
 
 
 def to_code(config):
+    cg.add_global(uart_ns.using)
     var = cg.new_Pvariable(config[CONF_ID])
     yield cg.register_component(var, config)
 
@@ -47,6 +50,7 @@ def to_code(config):
         cg.add(var.set_tx_pin(config[CONF_TX_PIN]))
     if CONF_RX_PIN in config:
         cg.add(var.set_rx_pin(config[CONF_RX_PIN]))
+    cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
 
 
 # A schema to use for all UART devices, all UART integrations must extend this!
diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp
index 56661b8aa7..3284d4cb67 100644
--- a/esphome/components/uart/uart.cpp
+++ b/esphome/components/uart/uart.cpp
@@ -2,6 +2,11 @@
 #include "esphome/core/log.h"
 #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 {
@@ -20,16 +25,15 @@ void UARTComponent::setup() {
   // is 1 we still want to use Serial.
   if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
     this->hw_serial_ = &Serial;
-  } else if (this->tx_pin_.value_or(9) == 9 && this->rx_pin_.value_or(10) == 10) {
-    this->hw_serial_ = &Serial1;
-  } else if (this->tx_pin_.value_or(16) == 16 && this->rx_pin_.value_or(17) == 17) {
-    this->hw_serial_ = &Serial2;
   } else {
     this->hw_serial_ = new HardwareSerial(next_uart_num++);
   }
   int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1;
   int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1;
-  this->hw_serial_->begin(this->baud_rate_, SERIAL_8N1, rx, tx);
+  uint32_t config = SERIAL_8N1;
+  if (this->stop_bits_ == 2)
+    config = SERIAL_8N2;
+  this->hw_serial_->begin(this->baud_rate_, config, rx, tx);
 }
 
 void UARTComponent::dump_config() {
@@ -41,6 +45,13 @@ void UARTComponent::dump_config() {
     ESP_LOGCONFIG(TAG, "  RX Pin: GPIO%d", *this->rx_pin_);
   }
   ESP_LOGCONFIG(TAG, "  Baud Rate: %u baud", this->baud_rate_);
+  ESP_LOGCONFIG(TAG, "  Stop bits: %u", this->stop_bits_);
+#ifdef USE_LOGGER
+  if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) {
+    ESP_LOGW(TAG, "  You're using the same serial port for logging and the UART component. Please "
+                  "disable logging over the serial port by setting logger->baud_rate to 0.");
+  }
+#endif
 }
 
 void UARTComponent::write_byte(uint8_t data) {
@@ -106,21 +117,27 @@ 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.
+  uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE;
+  if (this->stop_bits_ == 1)
+    mode |= UART_NB_STOP_BIT_1;
+  else
+    mode |= UART_NB_STOP_BIT_2;
+  SerialConfig config = static_cast<SerialConfig>(mode);
   if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
     this->hw_serial_ = &Serial;
-    this->hw_serial_->begin(this->baud_rate_);
+    this->hw_serial_->begin(this->baud_rate_, config);
   } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) {
     this->hw_serial_ = &Serial;
-    this->hw_serial_->begin(this->baud_rate_);
+    this->hw_serial_->begin(this->baud_rate_, config);
     this->hw_serial_->swap();
   } 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_);
+    this->hw_serial_->begin(this->baud_rate_, config);
   } else {
     this->sw_serial_ = new ESP8266SoftwareSerial();
     int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1;
     int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1;
-    this->sw_serial_->setup(tx, rx, this->baud_rate_);
+    this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_);
   }
 }
 
@@ -133,11 +150,19 @@ void UARTComponent::dump_config() {
     ESP_LOGCONFIG(TAG, "  RX Pin: GPIO%d", *this->rx_pin_);
   }
   ESP_LOGCONFIG(TAG, "  Baud Rate: %u baud", this->baud_rate_);
+  ESP_LOGCONFIG(TAG, "  Stop bits: %u", this->stop_bits_);
   if (this->hw_serial_ != nullptr) {
     ESP_LOGCONFIG(TAG, "  Using hardware serial interface.");
   } else {
     ESP_LOGCONFIG(TAG, "  Using software serial");
   }
+
+#ifdef USE_LOGGER
+  if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) {
+    ESP_LOGW(TAG, "  You're using the same serial port for logging and the UART component. Please "
+                  "disable logging over the serial port by setting logger->baud_rate to 0.");
+  }
+#endif
 }
 
 void UARTComponent::write_byte(uint8_t data) {
@@ -235,7 +260,7 @@ void UARTComponent::flush() {
   }
 }
 
-void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate) {
+void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) {
   this->bit_time_ = F_CPU / baud_rate;
   if (tx_pin != -1) {
     auto pin = GPIOPin(tx_pin, OUTPUT);
@@ -250,6 +275,7 @@ void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_ra
     this->rx_buffer_ = new uint8_t[this->rx_buffer_size_];
     pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING);
   }
+  this->stop_bits_ = stop_bits;
 }
 void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
   uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500;
@@ -266,6 +292,8 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg
   rec |= arg->read_bit_(&wait, start) << 7;
   // Stop bit
   arg->wait_(&wait, start);
+  if (arg->stop_bits_ == 2)
+    arg->wait_(&wait, start);
 
   arg->rx_buffer_[arg->rx_in_pos_] = rec;
   arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_;
@@ -293,14 +321,16 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
   this->write_bit_(data & (1 << 7), &wait, start);
   // Stop bit
   this->write_bit_(true, &wait, start);
+  if (this->stop_bits_ == 2)
+    this->wait_(&wait, start);
   enable_interrupts();
 }
-void ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) {
+void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) {
   while (ESP.getCycleCount() - start < *wait)
     ;
   *wait += this->bit_time_;
 }
-bool ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) {
+bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) {
   this->wait_(wait, start);
   return this->rx_pin_->digital_read();
 }
@@ -320,7 +350,9 @@ uint8_t ESP8266SoftwareSerial::peek_byte() {
     return 0;
   return this->rx_buffer_[this->rx_out_pos_];
 }
-void ESP8266SoftwareSerial::flush() { this->rx_in_pos_ = this->rx_out_pos_ = 0; }
+void ESP8266SoftwareSerial::flush() {
+  // Flush is a NO-OP with software serial, all bytes are written immediately.
+}
 int ESP8266SoftwareSerial::available() {
   int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_);
   if (avail < 0)
@@ -346,5 +378,16 @@ int UARTComponent::peek() {
   return data;
 }
 
+void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) {
+  if (this->parent_->baud_rate_ != baud_rate) {
+    ESP_LOGE(TAG, "  Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate,
+             this->parent_->baud_rate_);
+  }
+  if (this->parent_->stop_bits_ != stop_bits) {
+    ESP_LOGE(TAG, "  Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits,
+             this->parent_->stop_bits_);
+  }
+}
+
 }  // namespace uart
 }  // namespace esphome
diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h
index 666b8e2fb2..0e92fed0dc 100644
--- a/esphome/components/uart/uart.h
+++ b/esphome/components/uart/uart.h
@@ -10,7 +10,7 @@ namespace uart {
 #ifdef ARDUINO_ARCH_ESP8266
 class ESP8266SoftwareSerial {
  public:
-  void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate);
+  void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits);
 
   uint8_t read_byte();
   uint8_t peek_byte();
@@ -24,15 +24,16 @@ class ESP8266SoftwareSerial {
  protected:
   static void gpio_intr(ESP8266SoftwareSerial *arg);
 
-  inline void wait_(uint32_t *wait, const uint32_t &start);
-  inline bool read_bit_(uint32_t *wait, const uint32_t &start);
-  inline void write_bit_(bool bit, uint32_t *wait, const uint32_t &start);
+  void wait_(uint32_t *wait, const uint32_t &start);
+  bool read_bit_(uint32_t *wait, const uint32_t &start);
+  void write_bit_(bool bit, uint32_t *wait, const uint32_t &start);
 
   uint32_t bit_time_{0};
   uint8_t *rx_buffer_{nullptr};
-  size_t rx_buffer_size_{64};
+  size_t rx_buffer_size_{512};
   volatile size_t rx_in_pos_{0};
   size_t rx_out_pos_{0};
+  uint8_t stop_bits_;
   ISRInternalGPIOPin *tx_pin_{nullptr};
   ISRInternalGPIOPin *rx_pin_{nullptr};
 };
@@ -61,6 +62,7 @@ class UARTComponent : public Component, public Stream {
 
   int available() override;
 
+  /// Block until all bytes have been written to the UART bus.
   void flush() override;
 
   float get_setup_priority() const override { return setup_priority::BUS; }
@@ -71,9 +73,11 @@ class UARTComponent : public Component, public Stream {
 
   void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; }
   void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; }
+  void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; }
 
  protected:
   bool check_read_timeout_(size_t len = 1);
+  friend class UARTDevice;
 
   HardwareSerial *hw_serial_{nullptr};
 #ifdef ARDUINO_ARCH_ESP8266
@@ -82,6 +86,7 @@ class UARTComponent : public Component, public Stream {
   optional<uint8_t> tx_pin_;
   optional<uint8_t> rx_pin_;
   uint32_t baud_rate_;
+  uint8_t stop_bits_;
 };
 
 #ifdef ARDUINO_ARCH_ESP32
@@ -99,6 +104,9 @@ class UARTDevice : public Stream {
 
   void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); }
   void write_array(const std::vector<uint8_t> &data) { this->parent_->write_array(data); }
+  template<size_t N> void write_array(const std::array<uint8_t, N> &data) {
+    this->parent_->write_array(data.data(), data.size());
+  }
 
   void write_str(const char *str) { this->parent_->write_str(str); }
 
@@ -106,6 +114,13 @@ class UARTDevice : public Stream {
   bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); }
 
   bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); }
+  template<size_t N> optional<std::array<uint8_t, N>> read_array() {  // NOLINT
+    std::array<uint8_t, N> res;
+    if (!this->read_array(res.data(), N)) {
+      return {};
+    }
+    return res;
+  }
 
   int available() override { return this->parent_->available(); }
 
@@ -115,6 +130,9 @@ class UARTDevice : public Stream {
   int read() override { return this->parent_->read(); }
   int peek() override { return this->parent_->peek(); }
 
+  /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
+  void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1);
+
  protected:
   UARTComponent *parent_{nullptr};
 };
diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp
index 5d4cd48bc1..f8130f7d1f 100644
--- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp
+++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp
@@ -26,7 +26,6 @@ void UltrasonicSensorComponent::update() {
     this->publish_state(NAN);
   } else {
     float result = UltrasonicSensorComponent::us_to_m(time);
-    this->publish_state(result);
     ESP_LOGD(TAG, "'%s' - Got distance: %.2f m", this->name_.c_str(), result);
     this->publish_state(result);
   }
diff --git a/esphome/components/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp
index f047724768..5d117ab61d 100644
--- a/esphome/components/uptime/uptime_sensor.cpp
+++ b/esphome/components/uptime/uptime_sensor.cpp
@@ -27,6 +27,7 @@ void UptimeSensor::update() {
 }
 std::string UptimeSensor::unique_id() { return get_mac_address() + "-uptime"; }
 float UptimeSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
+void UptimeSensor::dump_config() { LOG_SENSOR("", "Uptime Sensor", this); }
 
 }  // namespace uptime
 }  // namespace esphome
diff --git a/esphome/components/uptime/uptime_sensor.h b/esphome/components/uptime/uptime_sensor.h
index 184022503d..dab380d2d9 100644
--- a/esphome/components/uptime/uptime_sensor.h
+++ b/esphome/components/uptime/uptime_sensor.h
@@ -9,6 +9,7 @@ namespace uptime {
 class UptimeSensor : public sensor::Sensor, public PollingComponent {
  public:
   void update() override;
+  void dump_config() override;
 
   float get_setup_priority() const override;
 
diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt
new file mode 100644
index 0000000000..fe33583414
--- /dev/null
+++ b/esphome/components/vl53l0x/LICENSE.txt
@@ -0,0 +1,80 @@
+Most of the code in this integration is based on the VL53L0x library
+by Pololu (Pololu Corporation), which in turn is based on the VL53L0X
+API from ST. The code has been adapted to work with ESPHome's i2c APIs.
+Please see the top-level LICENSE.txt for information about ESPHome's license.
+The licenses for Pololu's and ST's software are included below.
+Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019).
+
+=================================================================
+
+Copyright (c) 2017 Pololu Corporation.  For more information, see
+
+https://www.pololu.com/
+https://forum.pololu.com/
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+=================================================================
+
+Most of the functionality of this library is based on the VL53L0X
+API provided by ST (STSW-IMG005), and some of the explanatory
+comments are quoted or paraphrased from the API source code, API
+user manual (UM2039), and the VL53L0X datasheet.
+
+The following applies to source code reproduced or derived from
+the API:
+
+-----------------------------------------------------------------
+
+Copyright © 2016, STMicroelectronics International N.V.  All
+rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following
+disclaimer in the documentation and/or other materials provided
+with the distribution.
+* Neither the name of STMicroelectronics nor the
+names of its contributors may be used to endorse or promote
+products derived from this software without specific prior
+written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
+NON-INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS ARE DISCLAIMED.
+IN NO EVENT SHALL STMICROELECTRONICS INTERNATIONAL N.V. BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+-----------------------------------------------------------------
diff --git a/esphome/components/vl53l0x/__init__.py b/esphome/components/vl53l0x/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py
new file mode 100644
index 0000000000..6740d53e13
--- /dev/null
+++ b/esphome/components/vl53l0x/sensor.py
@@ -0,0 +1,24 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL
+
+DEPENDENCIES = ['i2c']
+
+vl53l0x_ns = cg.esphome_ns.namespace('vl53l0x')
+VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComponent,
+                                  i2c.I2CDevice)
+
+CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit'
+CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({
+    cv.GenerateID(): cv.declare_id(VL53L0XSensor),
+    cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range(
+        min=0.0, max=512.0, min_included=False, max_included=False)
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield sensor.register_sensor(var, config)
+    yield i2c.register_i2c_device(var, config)
diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp
new file mode 100644
index 0000000000..231bed99ac
--- /dev/null
+++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp
@@ -0,0 +1,249 @@
+#include "vl53l0x_sensor.h"
+#include "esphome/core/log.h"
+
+/*
+ * Most of the code in this integration is based on the VL53L0x library
+ * by Pololu (Pololu Corporation), which in turn is based on the VL53L0X
+ * API from ST.
+ *
+ * For more information about licensing, please view the included LICENSE.txt file
+ * in the vl53l0x integration directory.
+ */
+
+namespace esphome {
+namespace vl53l0x {
+
+static const char *TAG = "vl53l0x";
+
+void VL53L0XSensor::dump_config() {
+  LOG_SENSOR("", "VL53L0X", this);
+  LOG_UPDATE_INTERVAL(this);
+  LOG_I2C_DEVICE(this);
+}
+void VL53L0XSensor::setup() {
+  reg(0x89) |= 0x01;
+  reg(0x88) = 0x00;
+
+  reg(0x80) = 0x01;
+  reg(0xFF) = 0x01;
+  reg(0x00) = 0x00;
+  stop_variable_ = reg(0x91).get();
+
+  reg(0x00) = 0x01;
+  reg(0xFF) = 0x00;
+  reg(0x80) = 0x00;
+  reg(0x60) |= 0x12;
+
+  auto rate_value = static_cast<uint16_t>(signal_rate_limit_ * 128);
+  write_byte_16(0x44, rate_value);
+
+  reg(0x01) = 0xFF;
+
+  // getSpadInfo()
+  reg(0x80) = 0x01;
+  reg(0xFF) = 0x01;
+  reg(0x00) = 0x00;
+  reg(0xFF) = 0x06;
+  reg(0x83) |= 0x04;
+  reg(0xFF) = 0x07;
+  reg(0x81) = 0x01;
+  reg(0x80) = 0x01;
+  reg(0x94) = 0x6B;
+  reg(0x83) = 0x00;
+
+  while (reg(0x83).get() == 0x00)
+    yield();
+
+  reg(0x83) = 0x01;
+  uint8_t tmp = reg(0x92).get();
+  uint8_t spad_count = tmp & 0x7F;
+  bool spad_type_is_aperture = tmp & 0x80;
+
+  reg(0x81) = 0x00;
+  reg(0xFF) = 0x06;
+  reg(0x83) &= ~0x04;
+  reg(0xFF) = 0x01;
+  reg(0x00) = 0x01;
+  reg(0xFF) = 0x00;
+  reg(0x80) = 0x00;
+
+  uint8_t ref_spad_map[6];
+  this->read_bytes(0xB0, ref_spad_map, 6);
+
+  reg(0xFF) = 0x01;
+  reg(0x4F) = 0x00;
+  reg(0x4E) = 0x2C;
+  reg(0xFF) = 0x00;
+  reg(0xB6) = 0xB4;
+
+  uint8_t first_spad_to_enable = spad_type_is_aperture ? 12 : 0;
+  uint8_t spads_enabled = 0;
+  for (int i = 0; i < 48; i++) {
+    uint8_t &val = ref_spad_map[i / 8];
+    uint8_t mask = 1 << (i % 8);
+
+    if (i < first_spad_to_enable || spads_enabled == spad_count)
+      val &= ~mask;
+    else if (val & mask)
+      spads_enabled += 1;
+  }
+
+  this->write_bytes(0xB0, ref_spad_map, 6);
+
+  reg(0xFF) = 0x01;
+  reg(0x00) = 0x00;
+  reg(0xFF) = 0x00;
+  reg(0x09) = 0x00;
+  reg(0x10) = 0x00;
+  reg(0x11) = 0x00;
+  reg(0x24) = 0x01;
+  reg(0x25) = 0xFF;
+  reg(0x75) = 0x00;
+  reg(0xFF) = 0x01;
+  reg(0x4E) = 0x2C;
+  reg(0x48) = 0x00;
+  reg(0x30) = 0x20;
+  reg(0xFF) = 0x00;
+  reg(0x30) = 0x09;
+  reg(0x54) = 0x00;
+  reg(0x31) = 0x04;
+  reg(0x32) = 0x03;
+  reg(0x40) = 0x83;
+  reg(0x46) = 0x25;
+  reg(0x60) = 0x00;
+  reg(0x27) = 0x00;
+  reg(0x50) = 0x06;
+  reg(0x51) = 0x00;
+  reg(0x52) = 0x96;
+  reg(0x56) = 0x08;
+  reg(0x57) = 0x30;
+  reg(0x61) = 0x00;
+  reg(0x62) = 0x00;
+  reg(0x64) = 0x00;
+  reg(0x65) = 0x00;
+  reg(0x66) = 0xA0;
+  reg(0xFF) = 0x01;
+  reg(0x22) = 0x32;
+  reg(0x47) = 0x14;
+  reg(0x49) = 0xFF;
+  reg(0x4A) = 0x00;
+  reg(0xFF) = 0x00;
+  reg(0x7A) = 0x0A;
+  reg(0x7B) = 0x00;
+  reg(0x78) = 0x21;
+  reg(0xFF) = 0x01;
+  reg(0x23) = 0x34;
+  reg(0x42) = 0x00;
+  reg(0x44) = 0xFF;
+  reg(0x45) = 0x26;
+  reg(0x46) = 0x05;
+  reg(0x40) = 0x40;
+  reg(0x0E) = 0x06;
+  reg(0x20) = 0x1A;
+  reg(0x43) = 0x40;
+  reg(0xFF) = 0x00;
+  reg(0x34) = 0x03;
+  reg(0x35) = 0x44;
+  reg(0xFF) = 0x01;
+  reg(0x31) = 0x04;
+  reg(0x4B) = 0x09;
+  reg(0x4C) = 0x05;
+  reg(0x4D) = 0x04;
+  reg(0xFF) = 0x00;
+  reg(0x44) = 0x00;
+  reg(0x45) = 0x20;
+  reg(0x47) = 0x08;
+  reg(0x48) = 0x28;
+  reg(0x67) = 0x00;
+  reg(0x70) = 0x04;
+  reg(0x71) = 0x01;
+  reg(0x72) = 0xFE;
+  reg(0x76) = 0x00;
+  reg(0x77) = 0x00;
+  reg(0xFF) = 0x01;
+  reg(0x0D) = 0x01;
+  reg(0xFF) = 0x00;
+  reg(0x80) = 0x01;
+  reg(0x01) = 0xF8;
+  reg(0xFF) = 0x01;
+  reg(0x8E) = 0x01;
+  reg(0x00) = 0x01;
+  reg(0xFF) = 0x00;
+  reg(0x80) = 0x00;
+
+  reg(0x0A) = 0x04;
+  reg(0x84) &= ~0x10;
+  reg(0x0B) = 0x01;
+
+  measurement_timing_budget_us_ = get_measurement_timing_budget_();
+  reg(0x01) = 0xE8;
+  set_measurement_timing_budget_(measurement_timing_budget_us_);
+  reg(0x01) = 0x01;
+
+  if (!perform_single_ref_calibration_(0x40)) {
+    ESP_LOGW(TAG, "1st reference calibration failed!");
+    this->mark_failed();
+    return;
+  }
+  reg(0x01) = 0x02;
+  if (!perform_single_ref_calibration_(0x00)) {
+    ESP_LOGW(TAG, "2nd reference calibration failed!");
+    this->mark_failed();
+    return;
+  }
+  reg(0x01) = 0xE8;
+}
+void VL53L0XSensor::update() {
+  if (this->initiated_read_ || this->waiting_for_interrupt_) {
+    this->publish_state(NAN);
+    this->status_set_warning();
+  }
+
+  // initiate single shot measurement
+  reg(0x80) = 0x01;
+  reg(0xFF) = 0x01;
+
+  reg(0x00) = 0x00;
+  reg(0x91) = stop_variable_;
+  reg(0x00) = 0x01;
+  reg(0xFF) = 0x00;
+  reg(0x80) = 0x00;
+
+  reg(0x00) = 0x01;
+  this->waiting_for_interrupt_ = false;
+  this->initiated_read_ = true;
+  // wait for timeout
+}
+void VL53L0XSensor::loop() {
+  if (this->initiated_read_) {
+    if (reg(0x00).get() & 0x01) {
+      // waiting
+    } else {
+      // done
+      // wait until reg(0x13) & 0x07 is set
+      this->initiated_read_ = false;
+      this->waiting_for_interrupt_ = true;
+    }
+  }
+  if (this->waiting_for_interrupt_) {
+    if (reg(0x13).get() & 0x07) {
+      uint16_t range_mm;
+      this->read_byte_16(0x14 + 10, &range_mm);
+      reg(0x0B) = 0x01;
+      this->waiting_for_interrupt_ = false;
+
+      if (range_mm >= 8190) {
+        ESP_LOGW(TAG, "'%s' - Distance is out of range, please move the target closer", this->name_.c_str());
+        this->publish_state(NAN);
+        return;
+      }
+
+      float range_m = range_mm / 1e3f;
+      ESP_LOGD(TAG, "'%s' - Got distance %.3f m", this->name_.c_str(), range_m);
+      this->publish_state(range_m);
+    }
+  }
+}
+
+}  // namespace vl53l0x
+}  // namespace esphome
diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h
new file mode 100644
index 0000000000..1825383cee
--- /dev/null
+++ b/esphome/components/vl53l0x/vl53l0x_sensor.h
@@ -0,0 +1,257 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace vl53l0x {
+
+struct SequenceStepEnables {
+  bool tcc, msrc, dss, pre_range, final_range;
+};
+
+struct SequenceStepTimeouts {
+  uint16_t pre_range_vcsel_period_pclks, final_range_vcsel_period_pclks;
+
+  uint16_t msrc_dss_tcc_mclks, pre_range_mclks, final_range_mclks;
+  uint32_t msrc_dss_tcc_us, pre_range_us, final_range_us;
+};
+
+class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+ public:
+  void setup() override;
+
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void update() override;
+
+  void loop() override;
+
+  void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; }
+
+ protected:
+  uint32_t get_measurement_timing_budget_() {
+    SequenceStepEnables enables{};
+    SequenceStepTimeouts timeouts{};
+
+    uint16_t start_overhead = 1910;
+    uint16_t end_overhead = 960;
+    uint16_t msrc_overhead = 660;
+    uint16_t tcc_overhead = 590;
+    uint16_t dss_overhead = 690;
+    uint16_t pre_range_overhead = 660;
+    uint16_t final_range_overhead = 550;
+
+    // "Start and end overhead times always present"
+    uint32_t budget_us = start_overhead + end_overhead;
+
+    get_sequence_step_enables_(&enables);
+    get_sequence_step_timeouts_(&enables, &timeouts);
+
+    if (enables.tcc)
+      budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead);
+
+    if (enables.dss)
+      budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead);
+    else if (enables.msrc)
+      budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead);
+
+    if (enables.pre_range)
+      budget_us += (timeouts.pre_range_us + pre_range_overhead);
+
+    if (enables.final_range)
+      budget_us += (timeouts.final_range_us + final_range_overhead);
+
+    measurement_timing_budget_us_ = budget_us;  // store for internal reuse
+    return budget_us;
+  }
+
+  bool set_measurement_timing_budget_(uint32_t budget_us) {
+    SequenceStepEnables enables{};
+    SequenceStepTimeouts timeouts{};
+
+    uint16_t start_overhead = 1320;  // note that this is different than the value in get_
+    uint16_t end_overhead = 960;
+    uint16_t msrc_overhead = 660;
+    uint16_t tcc_overhead = 590;
+    uint16_t dss_overhead = 690;
+    uint16_t pre_range_overhead = 660;
+    uint16_t final_range_overhead = 550;
+
+    uint32_t min_timing_budget = 20000;
+
+    if (budget_us < min_timing_budget) {
+      return false;
+    }
+
+    uint32_t used_budget_us = start_overhead + end_overhead;
+
+    get_sequence_step_enables_(&enables);
+    get_sequence_step_timeouts_(&enables, &timeouts);
+
+    if (enables.tcc) {
+      used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead);
+    }
+
+    if (enables.dss) {
+      used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead);
+    } else if (enables.msrc) {
+      used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead);
+    }
+
+    if (enables.pre_range) {
+      used_budget_us += (timeouts.pre_range_us + pre_range_overhead);
+    }
+
+    if (enables.final_range) {
+      used_budget_us += final_range_overhead;
+
+      // "Note that the final range timeout is determined by the timing
+      // budget and the sum of all other timeouts within the sequence.
+      // If there is no room for the final range timeout, then an error
+      // will be set. Otherwise the remaining time will be applied to
+      // the final range."
+
+      if (used_budget_us > budget_us) {
+        // "Requested timeout too big."
+        return false;
+      }
+
+      uint32_t final_range_timeout_us = budget_us - used_budget_us;
+
+      // set_sequence_step_timeout() begin
+      // (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE)
+
+      // "For the final range timeout, the pre-range timeout
+      //  must be added. To do this both final and pre-range
+      //  timeouts must be expressed in macro periods MClks
+      //  because they have different vcsel periods."
+
+      uint16_t final_range_timeout_mclks =
+          timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks);
+
+      if (enables.pre_range) {
+        final_range_timeout_mclks += timeouts.pre_range_mclks;
+      }
+
+      write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks));
+
+      // set_sequence_step_timeout() end
+
+      measurement_timing_budget_us_ = budget_us;  // store for internal reuse
+    }
+    return true;
+  }
+
+  void get_sequence_step_enables_(SequenceStepEnables *enables) {
+    uint8_t sequence_config = reg(0x01).get();
+    enables->tcc = (sequence_config >> 4) & 0x1;
+    enables->dss = (sequence_config >> 3) & 0x1;
+    enables->msrc = (sequence_config >> 2) & 0x1;
+    enables->pre_range = (sequence_config >> 6) & 0x1;
+    enables->final_range = (sequence_config >> 7) & 0x1;
+  }
+
+  enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE };
+
+  void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) {
+    timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE);
+
+    timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1;
+    timeouts->msrc_dss_tcc_us =
+        timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks);
+
+    uint16_t value;
+    read_byte_16(0x51, &value);
+    timeouts->pre_range_mclks = decode_timeout_(value);
+    timeouts->pre_range_us =
+        timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks);
+
+    timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE);
+
+    read_byte_16(0x71, &value);
+    timeouts->final_range_mclks = decode_timeout_(value);
+
+    if (enables->pre_range) {
+      timeouts->final_range_mclks -= timeouts->pre_range_mclks;
+    }
+
+    timeouts->final_range_us =
+        timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks);
+  }
+
+  uint8_t get_vcsel_pulse_period_(VcselPeriodType type) {
+    uint8_t vcsel;
+    if (type == VCSEL_PERIOD_PRE_RANGE)
+      vcsel = reg(0x50).get();
+    else if (type == VCSEL_PERIOD_FINAL_RANGE)
+      vcsel = reg(0x70).get();
+    else
+      return 255;
+
+    return (vcsel + 1) << 1;
+  }
+
+  uint32_t get_macro_period_(uint8_t vcsel_period_pclks) {
+    return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL;
+  }
+
+  uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) {
+    uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks);
+    return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000;
+  }
+  uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) {
+    uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks);
+    return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns);
+  }
+
+  uint16_t decode_timeout_(uint16_t reg_val) {
+    // format: "(LSByte * 2^MSByte) + 1"
+    uint8_t msb = (reg_val >> 8) & 0xFF;
+    uint8_t lsb = (reg_val >> 0) & 0xFF;
+    return (uint16_t(lsb) << msb) + 1;
+  }
+  uint16_t encode_timeout_(uint16_t timeout_mclks) {
+    // format: "(LSByte * 2^MSByte) + 1"
+    uint32_t ls_byte = 0;
+    uint16_t ms_byte = 0;
+
+    if (timeout_mclks <= 0)
+      return 0;
+
+    ls_byte = timeout_mclks - 1;
+
+    while ((ls_byte & 0xFFFFFF00) > 0) {
+      ls_byte >>= 1;
+      ms_byte++;
+    }
+
+    return (ms_byte << 8) | (ls_byte & 0xFF);
+  }
+
+  bool perform_single_ref_calibration_(uint8_t vhv_init_byte) {
+    reg(0x00) = 0x01 | vhv_init_byte;  // VL53L0X_REG_SYSRANGE_MODE_START_STOP
+
+    uint32_t start = millis();
+    while ((reg(0x13).get() & 0x07) == 0) {
+      if (millis() - start > 1000)
+        return false;
+      yield();
+    }
+
+    reg(0x0B) = 0x01;
+    reg(0x00) = 0x00;
+
+    return true;
+  }
+
+  float signal_rate_limit_;
+  uint32_t measurement_timing_budget_us_;
+  bool initiated_read_{false};
+  bool waiting_for_interrupt_{false};
+  uint8_t stop_variable_;
+};
+
+}  // namespace vl53l0x
+}  // namespace esphome
diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index cb7de80918..a8ffbcc538 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -21,6 +21,7 @@ WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeBModel'
 MODELS = {
     '1.54in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN),
     '2.13in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN),
+    '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN),
     '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN),
     '2.70in': ('b', WaveshareEPaper2P7In),
     '4.20in': ('b', WaveshareEPaper4P2In),
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index 2fe12dc102..c2f7acde40 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -8,13 +8,45 @@ namespace waveshare_epaper {
 
 static const char *TAG = "waveshare_epaper";
 
-static const uint8_t FULL_UPDATE_LUT[30] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69,
-                                            0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00,
-                                            0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00};
+static const uint8_t LUT_SIZE_WAVESHARE = 30;
+static const uint8_t FULL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69,
+                                                            0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00,
+                                                            0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00};
 
-static const uint8_t PARTIAL_UPDATE_LUT[30] = {0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00,
-                                               0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                                               0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+static const uint8_t PARTIAL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = {
+    0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+static const uint8_t LUT_SIZE_TTGO = 70;
+static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = {
+    0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00,  // LUT0: BB:     VS 0 ~7
+    0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00,  // LUT1: BW:     VS 0 ~7
+    0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00,  // LUT2: WB:     VS 0 ~7
+    0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00,  // LUT3: WW:     VS 0 ~7
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT4: VCOM:   VS 0 ~7
+    0x03, 0x03, 0x00, 0x00, 0x02,              // TP0 A~D RP0
+    0x09, 0x09, 0x00, 0x00, 0x02,              // TP1 A~D RP1
+    0x03, 0x03, 0x00, 0x00, 0x02,              // TP2 A~D RP2
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP3 A~D RP3
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP4 A~D RP4
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP5 A~D RP5
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP6 A~D RP6
+};
+
+static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT0: BB:     VS 0 ~7
+    0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT1: BW:     VS 0 ~7
+    0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT2: WB:     VS 0 ~7
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT3: WW:     VS 0 ~7
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // LUT4: VCOM:   VS 0 ~7
+    0x0A, 0x00, 0x00, 0x00, 0x00,              // TP0 A~D RP0
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP1 A~D RP1
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP2 A~D RP2
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP3 A~D RP3
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP4 A~D RP4
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP5 A~D RP5
+    0x00, 0x00, 0x00, 0x00, 0x00,              // TP6 A~D RP6
+};
 
 void WaveshareEPaper::setup_pins_() {
   this->init_internal_(this->get_buffer_length_());
@@ -42,7 +74,6 @@ void WaveshareEPaper::data(uint8_t value) {
   this->write_byte(value);
   this->end_data_();
 }
-bool WaveshareEPaper::is_device_msb_first() { return true; }
 bool WaveshareEPaper::wait_until_idle_() {
   if (this->busy_pin_ == nullptr) {
     return true;
@@ -81,7 +112,6 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color)
     this->buffer_[pos] &= ~(0x80 >> subpos);
 }
 uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal() / 8u; }
-bool WaveshareEPaper::is_device_high_speed() { return true; }
 void WaveshareEPaper::start_command_() {
   this->dc_pin_->digital_write(false);
   this->enable();
@@ -136,6 +166,9 @@ void WaveshareEPaperTypeA::dump_config() {
     case WAVESHARE_EPAPER_2_13_IN:
       ESP_LOGCONFIG(TAG, "  Model: 2.13in");
       break;
+    case TTGO_EPAPER_2_13_IN:
+      ESP_LOGCONFIG(TAG, "  Model: 2.13in (TTGO)");
+      break;
     case WAVESHARE_EPAPER_2_9_IN:
       ESP_LOGCONFIG(TAG, "  Model: 2.9in");
       break;
@@ -156,7 +189,11 @@ void HOT WaveshareEPaperTypeA::display() {
     bool prev_full_update = this->at_update_ == 1;
     bool full_update = this->at_update_ == 0;
     if (full_update != prev_full_update) {
-      this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT);
+      if (this->model_ == TTGO_EPAPER_2_13_IN) {
+        this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO);
+      } else {
+        this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE);
+      }
     }
     this->at_update_ = (this->at_update_ + 1) % this->full_update_every_;
   }
@@ -208,6 +245,8 @@ int WaveshareEPaperTypeA::get_width_internal() {
       return 200;
     case WAVESHARE_EPAPER_2_13_IN:
       return 128;
+    case TTGO_EPAPER_2_13_IN:
+      return 128;
     case WAVESHARE_EPAPER_2_9_IN:
       return 128;
   }
@@ -219,15 +258,17 @@ int WaveshareEPaperTypeA::get_height_internal() {
       return 200;
     case WAVESHARE_EPAPER_2_13_IN:
       return 250;
+    case TTGO_EPAPER_2_13_IN:
+      return 250;
     case WAVESHARE_EPAPER_2_9_IN:
       return 296;
   }
   return 0;
 }
-void WaveshareEPaperTypeA::write_lut_(const uint8_t *lut) {
+void WaveshareEPaperTypeA::write_lut_(const uint8_t *lut, const uint8_t size) {
   // COMMAND WRITE LUT REGISTER
   this->command(0x32);
-  for (uint8_t i = 0; i < 30; i++)
+  for (uint8_t i = 0; i < size; i++)
     this->data(lut[i]);
 }
 WaveshareEPaperTypeA::WaveshareEPaperTypeA(WaveshareEPaperTypeAModel model) : model_(model) {}
@@ -495,7 +536,6 @@ void HOT WaveshareEPaper4P2In::display() {
 }
 int WaveshareEPaper4P2In::get_width_internal() { return 400; }
 int WaveshareEPaper4P2In::get_height_internal() { return 300; }
-bool WaveshareEPaper4P2In::is_device_high_speed() { return false; }
 void WaveshareEPaper4P2In::dump_config() {
   LOG_DISPLAY("", "Waveshare E-Paper", this);
   ESP_LOGCONFIG(TAG, "  Model: 4.2in");
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h
index 192e85275e..eff6b895a9 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.h
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.h
@@ -7,14 +7,16 @@
 namespace esphome {
 namespace waveshare_epaper {
 
-class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public display::DisplayBuffer {
+class WaveshareEPaper : public PollingComponent,
+                        public display::DisplayBuffer,
+                        public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                              spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
  public:
   void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
   float get_setup_priority() const override;
   void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
   void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
 
-  bool is_device_msb_first() override;
   void command(uint8_t value);
   void data(uint8_t value);
 
@@ -43,16 +45,14 @@ class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public d
   void reset_() {
     if (this->reset_pin_ != nullptr) {
       this->reset_pin_->digital_write(false);
-      delay(200);
+      delay(200);  // NOLINT
       this->reset_pin_->digital_write(true);
-      delay(200);
+      delay(200);  // NOLINT
     }
   }
 
   uint32_t get_buffer_length_();
 
-  bool is_device_high_speed() override;
-
   void start_command_();
   void end_command_();
   void start_data_();
@@ -67,6 +67,7 @@ enum WaveshareEPaperTypeAModel {
   WAVESHARE_EPAPER_1_54_IN = 0,
   WAVESHARE_EPAPER_2_13_IN,
   WAVESHARE_EPAPER_2_9_IN,
+  TTGO_EPAPER_2_13_IN,
 };
 
 class WaveshareEPaperTypeA : public WaveshareEPaper {
@@ -88,7 +89,7 @@ class WaveshareEPaperTypeA : public WaveshareEPaper {
   void set_full_update_every(uint32_t full_update_every);
 
  protected:
-  void write_lut_(const uint8_t *lut);
+  void write_lut_(const uint8_t *lut, uint8_t size);
 
   int get_width_internal() override;
 
@@ -143,7 +144,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper {
     // COMMAND PANEL SETTING
     this->command(0x00);
 
-    delay(100);
+    delay(100);  // NOLINT
 
     // COMMAND POWER SETTING
     this->command(0x01);
@@ -152,7 +153,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper {
     this->data(0x00);
     this->data(0x00);
     this->data(0x00);
-    delay(100);
+    delay(100);  // NOLINT
 
     // COMMAND POWER OFF
     this->command(0x02);
@@ -166,8 +167,6 @@ class WaveshareEPaper4P2In : public WaveshareEPaper {
   int get_width_internal() override;
 
   int get_height_internal() override;
-
-  bool is_device_high_speed() override;
 };
 
 class WaveshareEPaper7P5In : public WaveshareEPaper {
diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py
index 206fc2c733..04f3cc5c04 100644
--- a/esphome/components/web_server/__init__.py
+++ b/esphome/components/web_server/__init__.py
@@ -1,10 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.const import CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT
-from esphome.core import CORE, coroutine_with_priority
+from esphome.components import web_server_base
+from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
+from esphome.const import (
+    CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT,
+    CONF_AUTH, CONF_USERNAME, CONF_PASSWORD)
+from esphome.core import coroutine_with_priority
 
-DEPENDENCIES = ['network']
-AUTO_LOAD = ['json']
+AUTO_LOAD = ['json', 'web_server_base']
 
 web_server_ns = cg.esphome_ns.namespace('web_server')
 WebServer = web_server_ns.class_('WebServer', cg.Component, cg.Controller)
@@ -14,18 +17,25 @@ CONFIG_SCHEMA = cv.Schema({
     cv.Optional(CONF_PORT, default=80): cv.port,
     cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string,
     cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string,
+    cv.Optional(CONF_AUTH): cv.Schema({
+        cv.Required(CONF_USERNAME): cv.string_strict,
+        cv.Required(CONF_PASSWORD): cv.string_strict,
+    }),
+
+    cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase),
 }).extend(cv.COMPONENT_SCHEMA)
 
 
 @coroutine_with_priority(40.0)
 def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
+    paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
+
+    var = cg.new_Pvariable(config[CONF_ID], paren)
     yield cg.register_component(var, config)
 
-    cg.add(var.set_port(config[CONF_PORT]))
+    cg.add(paren.set_port(config[CONF_PORT]))
     cg.add(var.set_css_url(config[CONF_CSS_URL]))
     cg.add(var.set_js_url(config[CONF_JS_URL]))
-
-    if CORE.is_esp32:
-        cg.add_library('FS', None)
-    cg.add_library('ESP Async WebServer', '1.1.1')
+    if CONF_AUTH in config:
+        cg.add(var.set_username(config[CONF_AUTH][CONF_USERNAME]))
+        cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD]))
diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 882af4b995..4fdbbbce7d 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -6,15 +6,11 @@
 
 #include "StreamString.h"
 
-#ifdef ARDUINO_ARCH_ESP32
-#include <Update.h>
-#endif
-#ifdef ARDUINO_ARCH_ESP8266
-#include <Updater.h>
-#endif
-
 #include <cstdlib>
+
+#ifdef USE_LOGGER
 #include <esphome/components/logger/logger.h>
+#endif
 
 namespace esphome {
 namespace web_server {
@@ -66,7 +62,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
 
 void WebServer::setup() {
   ESP_LOGCONFIG(TAG, "Setting up web server...");
-  this->server_ = new AsyncWebServer(this->port_);
+  this->base_->init();
 
   this->events_.onConnect([this](AsyncEventSourceClient *client) {
     // Configure reconnect timeout
@@ -114,91 +110,21 @@ void WebServer::setup() {
     logger::global_logger->add_on_log_callback(
         [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); });
 #endif
-  this->server_->addHandler(this);
-  this->server_->addHandler(&this->events_);
-
-  this->server_->begin();
+  this->base_->add_handler(&this->events_);
+  this->base_->add_handler(this);
+  this->base_->add_ota_handler();
 
   this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); });
 }
 void WebServer::dump_config() {
   ESP_LOGCONFIG(TAG, "Web Server:");
-  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->port_);
+  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->base_->get_port());
+  if (this->using_auth()) {
+    ESP_LOGCONFIG(TAG, "  Basic authentication enabled");
+  }
 }
 float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
 
-void WebServer::handle_update_request(AsyncWebServerRequest *request) {
-  AsyncWebServerResponse *response;
-  if (!Update.hasError()) {
-    response = request->beginResponse(200, "text/plain", "Update Successful!");
-  } else {
-    StreamString ss;
-    ss.print("Update Failed: ");
-    Update.printError(ss);
-    response = request->beginResponse(200, "text/plain", ss);
-  }
-  response->addHeader("Connection", "close");
-  request->send(response);
-}
-
-void report_ota_error() {
-  StreamString ss;
-  Update.printError(ss);
-  ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
-}
-
-void WebServer::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data,
-                             size_t len, bool final) {
-  bool success;
-  if (index == 0) {
-    ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
-    this->ota_read_length_ = 0;
-#ifdef ARDUINO_ARCH_ESP8266
-    Update.runAsync(true);
-    success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
-#endif
-#ifdef ARDUINO_ARCH_ESP32
-    if (Update.isRunning())
-      Update.abort();
-    success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
-#endif
-    if (!success) {
-      report_ota_error();
-      return;
-    }
-  } else if (Update.hasError()) {
-    // don't spam logs with errors if something failed at start
-    return;
-  }
-
-  success = Update.write(data, len) == len;
-  if (!success) {
-    report_ota_error();
-    return;
-  }
-  this->ota_read_length_ += len;
-
-  const uint32_t now = millis();
-  if (now - this->last_ota_progress_ > 1000) {
-    if (request->contentLength() != 0) {
-      float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
-      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
-    } else {
-      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
-    }
-    this->last_ota_progress_ = now;
-  }
-
-  if (final) {
-    if (Update.end(true)) {
-      ESP_LOGI(TAG, "OTA update successful!");
-      this->set_timeout(100, []() { App.safe_reboot(); });
-    } else {
-      report_ota_error();
-    }
-  }
-}
-
 void WebServer::handle_index_request(AsyncWebServerRequest *request) {
   AsyncResponseStream *stream = request->beginResponseStream("text/html");
   std::string title = App.get_name() + " Web Server";
@@ -248,7 +174,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
 
   stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
                   "REST API documentation.</p>"
-                  "<h2>OTA Update</h2><form method='POST' action=\"/update\" enctype=\"multipart/form-data\"><input "
+                  "<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
                   "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"
                   "<h2>Debug Log</h2><pre id=\"log\"></pre>"
                   "<script src=\""));
@@ -490,11 +416,15 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch ma
       if (request->hasParam("color_temp"))
         call.set_color_temperature(request->getParam("color_temp")->value().toFloat());
 
-      if (request->hasParam("flash"))
-        call.set_flash_length((uint32_t) request->getParam("flash")->value().toFloat() * 1000);
+      if (request->hasParam("flash")) {
+        float length_s = request->getParam("flash")->value().toFloat();
+        call.set_flash_length(static_cast<uint32_t>(length_s * 1000));
+      }
 
-      if (request->hasParam("transition"))
-        call.set_transition_length((uint32_t) request->getParam("transition")->value().toFloat() * 1000);
+      if (request->hasParam("transition")) {
+        float length_s = request->getParam("transition")->value().toFloat();
+        call.set_transition_length(static_cast<uint32_t>(length_s * 1000));
+      }
 
       if (request->hasParam("effect")) {
         const char *effect = request->getParam("effect")->value().c_str();
@@ -531,9 +461,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
   if (request->url() == "/")
     return true;
 
-  if (request->url() == "/update" && request->method() == HTTP_POST)
-    return true;
-
   UrlMatch match = match_url(request->url().c_str(), true);
   if (!match.valid)
     return false;
@@ -570,13 +497,12 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
   return false;
 }
 void WebServer::handleRequest(AsyncWebServerRequest *request) {
-  if (request->url() == "/") {
-    this->handle_index_request(request);
-    return;
+  if (this->using_auth() && !request->authenticate(this->username_, this->password_)) {
+    return request->requestAuthentication();
   }
 
-  if (request->url() == "/update") {
-    this->handle_update_request(request);
+  if (request->url() == "/") {
+    this->handle_index_request(request);
     return;
   }
 
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 7840a81dce..4dca8200cc 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -2,9 +2,9 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/controller.h"
+#include "esphome/components/web_server_base/web_server_base.h"
 
 #include <vector>
-#include <ESPAsyncWebServer.h>
 
 namespace esphome {
 namespace web_server {
@@ -28,7 +28,11 @@ struct UrlMatch {
  */
 class WebServer : public Controller, public Component, public AsyncWebHandler {
  public:
-  void set_port(uint16_t port) { port_ = port; }
+  WebServer(web_server_base::WebServerBase *base) : base_(base) {}
+
+  void set_username(const char *username) { username_ = username; }
+
+  void set_password(const char *password) { password_ = password; }
 
   /** Set the URL to the CSS <link> that's sent to each client. Defaults to
    * https://esphome.io/_static/webserver-v1.min.css
@@ -57,7 +61,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   /// Handle an index request under '/'.
   void handle_index_request(AsyncWebServerRequest *request);
 
-  void handle_update_request(AsyncWebServerRequest *request);
+  bool using_auth() { return username_ != nullptr && password_ != nullptr; }
 
 #ifdef USE_SENSOR
   void on_sensor_update(sensor::Sensor *obj, float state) override;
@@ -122,19 +126,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   bool canHandle(AsyncWebServerRequest *request) override;
   /// Override the web handler's handleRequest method.
   void handleRequest(AsyncWebServerRequest *request) override;
-  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
-                    bool final) override;
   /// This web handle is not trivial.
   bool isRequestHandlerTrivial() override;
 
  protected:
-  uint16_t port_;
-  AsyncWebServer *server_;
+  web_server_base::WebServerBase *base_;
   AsyncEventSource events_{"/events"};
+  const char *username_{nullptr};
+  const char *password_{nullptr};
   const char *css_url_{nullptr};
   const char *js_url_{nullptr};
-  uint32_t last_ota_progress_{0};
-  uint32_t ota_read_length_{0};
 };
 
 }  // namespace web_server
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
new file mode 100644
index 0000000000..923a594eb8
--- /dev/null
+++ b/esphome/components/web_server_base/__init__.py
@@ -0,0 +1,26 @@
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome.const import CONF_ID
+from esphome.core import coroutine_with_priority, CORE
+
+DEPENDENCIES = ['network']
+AUTO_LOAD = ['async_tcp']
+
+web_server_base_ns = cg.esphome_ns.namespace('web_server_base')
+WebServerBase = web_server_base_ns.class_('WebServerBase', cg.Component)
+
+CONF_WEB_SERVER_BASE_ID = 'web_server_base_id'
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(WebServerBase),
+})
+
+
+@coroutine_with_priority(65.0)
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+
+    if CORE.is_esp32:
+        cg.add_library('FS', None)
+    # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json
+    cg.add_library('ESPAsyncWebServer-esphome', '1.2.5')
diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp
new file mode 100644
index 0000000000..b7548504e3
--- /dev/null
+++ b/esphome/components/web_server_base/web_server_base.cpp
@@ -0,0 +1,96 @@
+#include "web_server_base.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include <StreamString.h>
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <Update.h>
+#endif
+#ifdef ARDUINO_ARCH_ESP8266
+#include <Updater.h>
+#endif
+
+namespace esphome {
+namespace web_server_base {
+
+static const char *TAG = "web_server_base";
+
+void report_ota_error() {
+  StreamString ss;
+  Update.printError(ss);
+  ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
+}
+
+void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
+                                     uint8_t *data, size_t len, bool final) {
+  bool success;
+  if (index == 0) {
+    ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
+    this->ota_read_length_ = 0;
+#ifdef ARDUINO_ARCH_ESP8266
+    Update.runAsync(true);
+    success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+    if (Update.isRunning())
+      Update.abort();
+    success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
+#endif
+    if (!success) {
+      report_ota_error();
+      return;
+    }
+  } else if (Update.hasError()) {
+    // don't spam logs with errors if something failed at start
+    return;
+  }
+
+  success = Update.write(data, len) == len;
+  if (!success) {
+    report_ota_error();
+    return;
+  }
+  this->ota_read_length_ += len;
+
+  const uint32_t now = millis();
+  if (now - this->last_ota_progress_ > 1000) {
+    if (request->contentLength() != 0) {
+      float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
+      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
+    } else {
+      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
+    }
+    this->last_ota_progress_ = now;
+  }
+
+  if (final) {
+    if (Update.end(true)) {
+      ESP_LOGI(TAG, "OTA update successful!");
+      this->parent_->set_timeout(100, []() { App.safe_reboot(); });
+    } else {
+      report_ota_error();
+    }
+  }
+}
+void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
+  AsyncWebServerResponse *response;
+  if (!Update.hasError()) {
+    response = request->beginResponse(200, "text/plain", "Update Successful!");
+  } else {
+    StreamString ss;
+    ss.print("Update Failed: ");
+    Update.printError(ss);
+    response = request->beginResponse(200, "text/plain", ss);
+  }
+  response->addHeader("Connection", "close");
+  request->send(response);
+}
+
+void WebServerBase::add_ota_handler() { this->add_handler(new OTARequestHandler(this)); }
+float WebServerBase::get_setup_priority() const {
+  // Before WiFi (captive portal)
+  return setup_priority::WIFI + 2.0f;
+}
+
+}  // namespace web_server_base
+}  // namespace esphome
diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h
new file mode 100644
index 0000000000..b6024ceafa
--- /dev/null
+++ b/esphome/components/web_server_base/web_server_base.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "esphome/core/component.h"
+
+#include <ESPAsyncWebServer.h>
+
+namespace esphome {
+namespace web_server_base {
+
+class WebServerBase : public Component {
+ public:
+  void init() {
+    if (this->initialized_) {
+      this->initialized_++;
+      return;
+    }
+    this->server_ = new AsyncWebServer(this->port_);
+    this->server_->begin();
+
+    for (auto *handler : this->handlers_)
+      this->server_->addHandler(handler);
+
+    this->initialized_++;
+  }
+  void deinit() {
+    this->initialized_--;
+    if (this->initialized_ == 0) {
+      delete this->server_;
+      this->server_ = nullptr;
+    }
+  }
+  AsyncWebServer *get_server() const { return server_; }
+  float get_setup_priority() const override;
+
+  void add_handler(AsyncWebHandler *handler) {
+    // remove all handlers
+
+    this->handlers_.push_back(handler);
+    if (this->server_ != nullptr)
+      this->server_->addHandler(handler);
+  }
+
+  void add_ota_handler();
+
+  void set_port(uint16_t port) { port_ = port; }
+  uint16_t get_port() const { return port_; }
+
+ protected:
+  friend class OTARequestHandler;
+
+  int initialized_{0};
+  uint16_t port_{80};
+  AsyncWebServer *server_{nullptr};
+  std::vector<AsyncWebHandler *> handlers_;
+};
+
+class OTARequestHandler : public AsyncWebHandler {
+ public:
+  OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
+  void handleRequest(AsyncWebServerRequest *request) override;
+  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
+                    bool final) override;
+  bool canHandle(AsyncWebServerRequest *request) override {
+    return request->url() == "/update" && request->method() == HTTP_POST;
+  }
+
+  bool isRequestHandlerTrivial() override { return false; }
+
+ protected:
+  uint32_t last_ota_progress_{0};
+  uint32_t ota_read_length_{0};
+  WebServerBase *parent_;
+};
+
+}  // namespace web_server_base
+}  // namespace esphome
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index 9ac8b74ef2..93f2d59564 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -5,7 +5,7 @@ from esphome.automation import Condition
 from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \
     CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \
     CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \
-    CONF_SUBNET, CONF_USE_ADDRESS
+    CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY
 from esphome.core import CORE, HexInt, coroutine_with_priority
 
 AUTO_LOAD = ['network']
@@ -64,13 +64,15 @@ WIFI_NETWORK_BASE = cv.Schema({
     cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
 })
 
+CONF_AP_TIMEOUT = 'ap_timeout'
 WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
-
+    cv.Optional(CONF_AP_TIMEOUT, default='1min'): cv.positive_time_period_milliseconds,
 })
 
 WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
     cv.Optional(CONF_BSSID): cv.mac_address,
     cv.Optional(CONF_HIDDEN): cv.boolean,
+    cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
 })
 
 
@@ -118,8 +120,9 @@ CONFIG_SCHEMA = cv.All(cv.Schema({
 
     cv.Optional(CONF_AP): WIFI_NETWORK_AP,
     cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
-    cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds,
-    cv.Optional(CONF_POWER_SAVE_MODE, default='NONE'): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
+    cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds,
+    cv.SplitDefault(CONF_POWER_SAVE_MODE, esp8266='none', esp32='light'):
+        cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
     cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
     cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
 
@@ -160,25 +163,28 @@ def wifi_network(config, static_ip):
         cg.add(ap.set_channel(config[CONF_CHANNEL]))
     if static_ip is not None:
         cg.add(ap.set_manual_ip(manual_ip(static_ip)))
+    if CONF_PRIORITY in config:
+        cg.add(ap.set_priority(config[CONF_PRIORITY]))
 
     return ap
 
 
 @coroutine_with_priority(60.0)
 def to_code(config):
-    rhs = WiFiComponent.new()
-    wifi = cg.Pvariable(config[CONF_ID], rhs)
-    cg.add(wifi.set_use_address(config[CONF_USE_ADDRESS]))
+    var = cg.new_Pvariable(config[CONF_ID])
+    cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
 
     for network in config.get(CONF_NETWORKS, []):
-        cg.add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP))))
+        cg.add(var.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP))))
 
     if CONF_AP in config:
-        cg.add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP))))
+        conf = config[CONF_AP]
+        cg.add(var.set_ap(wifi_network(conf, config.get(CONF_MANUAL_IP))))
+        cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT]))
 
-    cg.add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
-    cg.add(wifi.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
-    cg.add(wifi.set_fast_connect(config[CONF_FAST_CONNECT]))
+    cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
+    cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
+    cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))
 
     if CORE.is_esp8266:
         cg.add_library('ESP8266WiFi', None)
@@ -186,7 +192,7 @@ def to_code(config):
     cg.add_define('USE_WIFI')
 
     # Register at end for OTA safe mode
-    yield cg.register_component(wifi, config)
+    yield cg.register_component(var, config)
 
 
 @automation.register_condition('wifi.connected', WiFiConnectedCondition, cv.Schema({}))
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 28ff54826f..cb664d3cc3 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -18,6 +18,10 @@
 #include "esphome/core/util.h"
 #include "esphome/core/application.h"
 
+#ifdef USE_CAPTIVE_PORTAL
+#include "esphome/components/captive_portal/captive_portal.h"
+#endif
+
 namespace esphome {
 namespace wifi {
 
@@ -27,20 +31,15 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
 
 void WiFiComponent::setup() {
   ESP_LOGCONFIG(TAG, "Setting up WiFi...");
-
-  this->wifi_register_callbacks_();
-
-  bool ret = this->wifi_mode_(this->has_sta(), false);
-  if (!ret) {
-    this->mark_failed();
-    return;
-  }
+  this->last_connected_ = millis();
+  this->wifi_pre_setup_();
 
   if (this->has_sta()) {
-    this->wifi_disable_auto_connect_();
-    delay(10);
+    this->wifi_sta_pre_setup_();
 
-    this->wifi_apply_power_save_();
+    if (!this->wifi_apply_power_save_()) {
+      ESP_LOGV(TAG, "Setting Power Save Option failed!");
+    }
 
     if (this->fast_connect_) {
       this->selected_ap_ = this->sta_[0];
@@ -50,10 +49,16 @@ void WiFiComponent::setup() {
     }
   } else if (this->has_ap()) {
     this->setup_ap_config_();
+#ifdef USE_CAPTIVE_PORTAL
+    if (captive_portal::global_captive_portal != nullptr)
+      captive_portal::global_captive_portal->start();
+#endif
   }
 
   this->wifi_apply_hostname_();
+#ifdef ARDUINO_ARCH_ESP32
   network_setup_mdns();
+#endif
 }
 
 void WiFiComponent::loop() {
@@ -99,6 +104,17 @@ void WiFiComponent::loop() {
         break;
     }
 
+    if (this->has_ap() && !this->ap_setup_) {
+      if (now - this->last_connected_ > this->ap_timeout_) {
+        ESP_LOGI(TAG, "Starting fallback AP!");
+        this->setup_ap_config_();
+#ifdef USE_CAPTIVE_PORTAL
+        if (captive_portal::global_captive_portal != nullptr)
+          captive_portal::global_captive_portal->start();
+#endif
+      }
+    }
+
     if (!this->has_ap() && this->reboot_timeout_ != 0) {
       if (now - this->last_connected_ > this->reboot_timeout_) {
         ESP_LOGE(TAG, "Can't connect to WiFi, rebooting...");
@@ -119,7 +135,7 @@ IPAddress WiFiComponent::get_ip_address() {
   if (this->has_sta())
     return this->wifi_sta_ip_();
   if (this->has_ap())
-    return this->wifi_soft_ap_ip_();
+    return this->wifi_soft_ap_ip();
   return {};
 }
 std::string WiFiComponent::get_use_address() const {
@@ -147,7 +163,10 @@ void WiFiComponent::setup_ap_config_() {
   }
 
   this->ap_setup_ = this->wifi_start_ap_(this->ap_);
-  ESP_LOGCONFIG(TAG, "  IP Address: %s", this->wifi_soft_ap_ip_().toString().c_str());
+  ESP_LOGCONFIG(TAG, "  IP Address: %s", this->wifi_soft_ap_ip().toString().c_str());
+#ifdef ARDUINO_ARCH_ESP8266
+  network_setup_mdns(this->wifi_soft_ap_ip(), 1);
+#endif
 
   if (!this->has_sta()) {
     this->state_ = WIFI_COMPONENT_STATE_AP;
@@ -159,6 +178,10 @@ float WiFiComponent::get_loop_priority() const {
 }
 void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; }
 void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
+void WiFiComponent::set_sta(const WiFiAP &ap) {
+  this->sta_.clear();
+  this->add_sta(ap);
+}
 
 void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
   ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str());
@@ -260,6 +283,9 @@ void WiFiComponent::print_connect_params_() {
   int8_t rssi = WiFi.RSSI();
   print_signal_bars(rssi, signal_bars);
   ESP_LOGCONFIG(TAG, "  Signal strength: %d dB %s", rssi, signal_bars);
+  if (this->selected_ap_.get_bssid().has_value()) {
+    ESP_LOGV(TAG, "  Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid()));
+  }
   ESP_LOGCONFIG(TAG, "  Channel: %d", WiFi.channel());
   ESP_LOGCONFIG(TAG, "  Subnet: %s", WiFi.subnetMask().toString().c_str());
   ESP_LOGCONFIG(TAG, "  Gateway: %s", WiFi.gatewayIP().toString().c_str());
@@ -295,6 +321,10 @@ void WiFiComponent::check_scanning_finished() {
     for (auto &ap : this->sta_) {
       if (res.matches(ap)) {
         res.set_matches(true);
+        if (!this->has_sta_priority(res.get_bssid())) {
+          this->set_sta_priority(res.get_bssid(), ap.get_priority());
+        }
+        res.set_priority(this->get_sta_priority(res.get_bssid()));
         break;
       }
     }
@@ -302,11 +332,18 @@ void WiFiComponent::check_scanning_finished() {
 
   std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(),
                    [](const WiFiScanResult &a, const WiFiScanResult &b) {
+                     // return true if a is better than b
                      if (a.get_matches() && !b.get_matches())
                        return true;
                      if (!a.get_matches() && b.get_matches())
                        return false;
 
+                     if (a.get_matches() && b.get_matches()) {
+                       // if both match, check priority
+                       if (a.get_priority() != b.get_priority())
+                         return a.get_priority() > b.get_priority();
+                     }
+
                      return a.get_rssi() > b.get_rssi();
                    });
 
@@ -377,13 +414,21 @@ void WiFiComponent::check_connecting_finished() {
   wl_status_t status = this->wifi_sta_status_();
 
   if (status == WL_CONNECTED) {
-    ESP_LOGI(TAG, "WiFi connected!");
+    ESP_LOGI(TAG, "WiFi Connected!");
     this->print_connect_params_();
 
     if (this->has_ap()) {
+#ifdef USE_CAPTIVE_PORTAL
+      if (this->is_captive_portal_active_()) {
+        captive_portal::global_captive_portal->end();
+      }
+#endif
       ESP_LOGD(TAG, "Disabling AP...");
       this->wifi_mode_({}, false);
     }
+#ifdef ARDUINO_ARCH_ESP8266
+    network_setup_mdns(this->wifi_sta_ip_(), 0);
+#endif
     this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
     this->num_retried_ = 0;
     return;
@@ -425,12 +470,18 @@ void WiFiComponent::check_connecting_finished() {
 }
 
 void WiFiComponent::retry_connect() {
+  if (this->selected_ap_.get_bssid()) {
+    auto bssid = *this->selected_ap_.get_bssid();
+    float priority = this->get_sta_priority(bssid);
+    this->set_sta_priority(bssid, priority - 1.0f);
+  }
+
   delay(10);
-  if (this->num_retried_ > 5 || this->error_from_callback_) {
+  if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) {
     // If retry failed for more than 5 times, let's restart STA
     ESP_LOGW(TAG, "Restarting WiFi adapter...");
     this->wifi_mode_(false, {});
-    delay(100);
+    delay(100);  // NOLINT
     this->num_retried_ = 0;
   } else {
     this->num_retried_++;
@@ -443,9 +494,6 @@ void WiFiComponent::retry_connect() {
     return;
   }
 
-  if (this->has_ap()) {
-    this->setup_ap_config_();
-  }
   this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
   this->action_started_ = millis();
 }
@@ -461,11 +509,6 @@ bool WiFiComponent::is_connected() {
   return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED &&
          !this->error_from_callback_;
 }
-bool WiFiComponent::ready_for_ota() {
-  if (this->has_ap())
-    return true;
-  return this->is_connected();
-}
 void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; }
 
 std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
@@ -473,20 +516,12 @@ std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
   sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
   return buf;
 }
-
-bool sta_field_equal(const uint8_t *field_a, const uint8_t *field_b, int len) {
-  for (int i = 0; i < len; i++) {
-    uint8_t a = field_a[i];
-    uint8_t b = field_b[i];
-    if (a == b && a == 0)
-      break;
-    if (a == b)
-      continue;
-
-    return false;
-  }
-
-  return true;
+bool WiFiComponent::is_captive_portal_active_() {
+#ifdef USE_CAPTIVE_PORTAL
+  return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();
+#else
+  return false;
+#endif
 }
 
 void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index 8e6418791c..04866ef8e2 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -66,12 +66,14 @@ class WiFiAP {
   void set_bssid(optional<bssid_t> bssid);
   void set_password(const std::string &password);
   void set_channel(optional<uint8_t> channel);
+  void set_priority(float priority) { priority_ = priority; }
   void set_manual_ip(optional<ManualIP> manual_ip);
   void set_hidden(bool hidden);
   const std::string &get_ssid() const;
   const optional<bssid_t> &get_bssid() const;
   const std::string &get_password() const;
   const optional<uint8_t> &get_channel() const;
+  float get_priority() const { return priority_; }
   const optional<ManualIP> &get_manual_ip() const;
   bool get_hidden() const;
 
@@ -80,6 +82,7 @@ class WiFiAP {
   optional<bssid_t> bssid_;
   std::string password_;
   optional<uint8_t> channel_;
+  float priority_{0};
   optional<ManualIP> manual_ip_;
   bool hidden_{false};
 };
@@ -99,6 +102,8 @@ class WiFiScanResult {
   int8_t get_rssi() const;
   bool get_with_auth() const;
   bool get_is_hidden() const;
+  float get_priority() const { return priority_; }
+  void set_priority(float priority) { priority_ = priority; }
 
  protected:
   bool matches_{false};
@@ -108,6 +113,12 @@ class WiFiScanResult {
   int8_t rssi_;
   bool with_auth_;
   bool is_hidden_;
+  float priority_{0.0f};
+};
+
+struct WiFiSTAPriority {
+  bssid_t bssid;
+  float priority;
 };
 
 enum WiFiPowerSaveMode {
@@ -122,6 +133,7 @@ class WiFiComponent : public Component {
   /// Construct a WiFiComponent.
   WiFiComponent();
 
+  void set_sta(const WiFiAP &ap);
   void add_sta(const WiFiAP &ap);
 
   /** Setup an Access Point that should be created if no connection to a station can be made.
@@ -137,6 +149,7 @@ class WiFiComponent : public Component {
   void check_scanning_finished();
   void start_connecting(const WiFiAP &ap, bool two);
   void set_fast_connect(bool fast_connect);
+  void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
 
   void check_connecting_finished();
 
@@ -144,8 +157,6 @@ class WiFiComponent : public Component {
 
   bool can_proceed() override;
 
-  bool ready_for_ota();
-
   void set_reboot_timeout(uint32_t reboot_timeout);
 
   bool is_connected();
@@ -171,24 +182,54 @@ class WiFiComponent : public Component {
   std::string get_use_address() const;
   void set_use_address(const std::string &use_address);
 
+  const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
+
+  IPAddress wifi_soft_ap_ip();
+
+  bool has_sta_priority(const bssid_t &bssid) {
+    for (auto &it : this->sta_priorities_)
+      if (it.bssid == bssid)
+        return true;
+    return false;
+  }
+  float get_sta_priority(const bssid_t bssid) {
+    for (auto &it : this->sta_priorities_)
+      if (it.bssid == bssid)
+        return it.priority;
+    return 0.0f;
+  }
+  void set_sta_priority(const bssid_t bssid, float priority) {
+    for (auto &it : this->sta_priorities_)
+      if (it.bssid == bssid) {
+        it.priority = priority;
+        return;
+      }
+    this->sta_priorities_.push_back(WiFiSTAPriority{
+        .bssid = bssid,
+        .priority = priority,
+    });
+  }
+
  protected:
   static std::string format_mac_addr(const uint8_t mac[6]);
   void setup_ap_config_();
   void print_connect_params_();
 
   bool wifi_mode_(optional<bool> sta, optional<bool> ap);
-  bool wifi_disable_auto_connect_();
+  bool wifi_sta_pre_setup_();
   bool wifi_apply_power_save_();
   bool wifi_sta_ip_config_(optional<ManualIP> manual_ip);
   IPAddress wifi_sta_ip_();
   bool wifi_apply_hostname_();
   bool wifi_sta_connect_(WiFiAP ap);
-  void wifi_register_callbacks_();
+  void wifi_pre_setup_();
   wl_status_t wifi_sta_status_();
   bool wifi_scan_start_();
   bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
   bool wifi_start_ap_(const WiFiAP &ap);
-  IPAddress wifi_soft_ap_ip_();
+  bool wifi_disconnect_();
+
+  bool is_captive_portal_active_();
 
 #ifdef ARDUINO_ARCH_ESP8266
   static void wifi_event_callback(System_Event_t *event);
@@ -203,6 +244,7 @@ class WiFiComponent : public Component {
 
   std::string use_address_;
   std::vector<WiFiAP> sta_;
+  std::vector<WiFiSTAPriority> sta_priorities_;
   WiFiAP selected_ap_;
   bool fast_connect_{false};
 
@@ -211,7 +253,8 @@ class WiFiComponent : public Component {
   uint32_t action_started_;
   uint8_t num_retried_{0};
   uint32_t last_connected_{0};
-  uint32_t reboot_timeout_{300000};
+  uint32_t reboot_timeout_{};
+  uint32_t ap_timeout_{};
   WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
   bool error_from_callback_{false};
   std::vector<WiFiScanResult> scan_result_;
diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp
index 6b118cf0ae..353a51040a 100644
--- a/esphome/components/wifi/wifi_component_esp32.cpp
+++ b/esphome/components/wifi/wifi_component_esp32.cpp
@@ -53,8 +53,12 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
 
   return ret;
 }
-bool WiFiComponent::wifi_disable_auto_connect_() {
+bool WiFiComponent::wifi_sta_pre_setup_() {
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
   WiFi.setAutoReconnect(false);
+  delay(10);
   return true;
 }
 bool WiFiComponent::wifi_apply_power_save_() {
@@ -158,10 +162,16 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
     conf.sta.channel = *ap.get_channel();
   }
 
-  esp_err_t err = esp_wifi_disconnect();
-  if (err != ESP_OK) {
-    ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err);
-    return false;
+  wifi_config_t current_conf;
+  esp_err_t err;
+  esp_wifi_get_config(WIFI_IF_STA, &current_conf);
+
+  if (memcmp(&current_conf, &conf, sizeof(wifi_config_t)) != 0) {
+    err = esp_wifi_disconnect();
+    if (err != ESP_OK) {
+      ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err);
+      return false;
+    }
   }
 
   err = esp_wifi_set_config(WIFI_IF_STA, &conf);
@@ -359,7 +369,7 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i
     }
     case SYSTEM_EVENT_AP_PROBEREQRECVED: {
       auto it = info.ap_probereqrecved;
-      ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
       break;
     }
     default:
@@ -382,10 +392,12 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i
     this->wifi_scan_done_callback_();
   }
 }
-void WiFiComponent::wifi_register_callbacks_() {
+void WiFiComponent::wifi_pre_setup_() {
   auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
   WiFi.onEvent(f);
   WiFi.persistent(false);
+  // Make sure WiFi is in clean state before anything starts
+  this->wifi_mode_(false, false);
 }
 wl_status_t WiFiComponent::wifi_sta_status_() { return WiFi.status(); }
 bool WiFiComponent::wifi_scan_start_() {
@@ -517,11 +529,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   return true;
 }
-IPAddress WiFiComponent::wifi_soft_ap_ip_() {
+IPAddress WiFiComponent::wifi_soft_ap_ip() {
   tcpip_adapter_ip_info_t ip;
   tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
   return IPAddress(ip.ip.addr);
 }
+bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
 
 }  // namespace wifi
 }  // namespace esphome
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index afd5b7c0cc..ed6e616b3d 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -58,19 +58,6 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
 
   return ret;
 }
-bool WiFiComponent::wifi_disable_auto_connect_() {
-  bool ret1, ret2;
-  ETS_UART_INTR_DISABLE();
-  ret1 = wifi_station_set_auto_connect(0);
-  ret2 = wifi_station_set_reconnect_policy(false);
-  ETS_UART_INTR_ENABLE();
-
-  if (!ret1 || !ret2) {
-    ESP_LOGV(TAG, "Disabling Auto-Connect failed!");
-  }
-
-  return ret1 && ret2;
-}
 bool WiFiComponent::wifi_apply_power_save_() {
   sleep_type_t power_save;
   switch (this->power_save_) {
@@ -158,9 +145,7 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) {
   if (!this->wifi_mode_(true, {}))
     return false;
 
-  ETS_UART_INTR_DISABLE();
-  wifi_station_disconnect();
-  ETS_UART_INTR_ENABLE();
+  this->wifi_disconnect_();
 
   struct station_config conf {};
   memset(&conf, 0, sizeof(conf));
@@ -330,11 +315,6 @@ const char *get_disconnect_reason_str(uint8_t reason) {
 }
 
 void WiFiComponent::wifi_event_callback(System_Event_t *event) {
-  // TODO: this callback is called while in cont context, so delay will fail
-  // We need to defer the log messages until we're out of this context
-  // only affects verbose log level
-  // reproducible by enabling verbose log level and letting the ESP disconnect and
-  // then reconnect to WiFi.
   switch (event->event) {
     case EVENT_STAMODE_CONNECTED: {
       auto it = event->event_info.connected;
@@ -382,7 +362,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
     }
     case EVENT_SOFTAPMODE_PROBEREQRECVED: {
       auto it = event->event_info.ap_probereqrecved;
-      ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      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
@@ -410,7 +390,62 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
   WiFiMockClass::_event_callback(event);
 }
 
-void WiFiComponent::wifi_register_callbacks_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); }
+bool WiFiComponent::wifi_sta_pre_setup_() {
+  if (!this->wifi_mode_(true, {}))
+    return false;
+
+  // Clear saved STA config
+  station_config default_config{};
+  wifi_station_get_config_default(&default_config);
+  bool is_zero = default_config.ssid[0] == '\0' && default_config.password[0] == '\0' && default_config.bssid[0] == 0 &&
+                 default_config.bssid_set == 0;
+  if (!is_zero) {
+    ESP_LOGV(TAG, "Clearing default wifi STA config");
+
+    memset(&default_config, 0, sizeof(default_config));
+    ETS_UART_INTR_DISABLE();
+    bool ret = wifi_station_set_config(&default_config);
+    ETS_UART_INTR_ENABLE();
+
+    if (!ret) {
+      ESP_LOGW(TAG, "Clearing default wif STA config failed!");
+    }
+  }
+
+  bool ret1, ret2;
+  ETS_UART_INTR_DISABLE();
+  ret1 = wifi_station_set_auto_connect(0);
+  ret2 = wifi_station_set_reconnect_policy(false);
+  ETS_UART_INTR_ENABLE();
+
+  if (!ret1 || !ret2) {
+    ESP_LOGV(TAG, "Disabling Auto-Connect failed!");
+  }
+
+  delay(10);
+  return true;
+}
+
+void WiFiComponent::wifi_pre_setup_() {
+  wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback);
+  // Make sure the default opmode is OFF
+  uint8_t default_opmode = wifi_get_opmode_default();
+  if (default_opmode != 0) {
+    ESP_LOGV(TAG, "Setting default WiFi Mode to 0 (was %u)", default_opmode);
+
+    ETS_UART_INTR_DISABLE();
+    bool ret = wifi_set_opmode(0);
+    ETS_UART_INTR_ENABLE();
+
+    if (!ret) {
+      ESP_LOGW(TAG, "Setting default WiFi mode failed!");
+    }
+  }
+
+  // Make sure WiFi is in clean state before anything starts
+  this->wifi_mode_(false, false);
+}
+
 wl_status_t WiFiComponent::wifi_sta_status_() {
   station_status_t status = wifi_station_get_connect_status();
   switch (status) {
@@ -435,11 +470,6 @@ bool WiFiComponent::wifi_scan_start_() {
   if (!this->wifi_mode_(true, {}))
     return false;
 
-  station_status_t sta_status = wifi_station_get_connect_status();
-  if (sta_status != STATION_GOT_IP && sta_status != STATION_IDLE) {
-    wifi_station_disconnect();
-  }
-
   struct scan_config config {};
   memset(&config, 0, sizeof(config));
   config.ssid = nullptr;
@@ -465,6 +495,15 @@ bool WiFiComponent::wifi_scan_start_() {
 
   return ret;
 }
+bool WiFiComponent::wifi_disconnect_() {
+  station_config conf{};
+  memset(&conf, 0, sizeof(conf));
+  ETS_UART_INTR_DISABLE();
+  wifi_station_set_config(&conf);
+  bool ret = wifi_station_disconnect();
+  ETS_UART_INTR_ENABLE();
+  return ret;
+}
 void WiFiComponent::s_wifi_scan_done_callback(void *arg, STATUS status) {
   global_wifi_component->wifi_scan_done_callback_(arg, status);
 }
@@ -583,7 +622,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   return true;
 }
-IPAddress WiFiComponent::wifi_soft_ap_ip_() {
+IPAddress WiFiComponent::wifi_soft_ap_ip() {
   struct ip_info ip {};
   wifi_get_ip_info(SOFTAP_IF, &ip);
   return {ip.ip.addr};
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
new file mode 100644
index 0000000000..704d9b3099
--- /dev/null
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
@@ -0,0 +1,14 @@
+#include "wifi_info_text_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace wifi_info {
+
+static const char *TAG = "wifi_info";
+
+void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); }
+void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
+void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
+
+}  // namespace wifi_info
+}  // namespace esphome
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h
index 13e632bde1..9dfa684b4b 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.h
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.h
@@ -17,6 +17,8 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
     }
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+  std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; }
+  void dump_config() override;
 
  protected:
   IPAddress last_ip_;
@@ -32,6 +34,8 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor {
     }
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+  std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; }
+  void dump_config() override;
 
  protected:
   std::string last_ssid_;
@@ -49,6 +53,8 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
     }
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+  std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; }
+  void dump_config() override;
 
  protected:
   wifi::bssid_t last_bssid_;
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 4367b5fd1e..1172f6ee0a 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -81,15 +81,17 @@ optional<XiaomiParseResult> parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d
     return {};
   }
 
-  bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01;
-  bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00;
+  bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01;
+  bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00;
+  bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04;
+  bool is_cgg1 = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03;
 
-  if (!is_mijia && !is_miflora) {
+  if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) {
     // ESP_LOGVV(TAG, "Xiaomi no magic bytes");
     return {};
   }
 
-  uint8_t raw_offset = is_mijia ? 11 : 12;
+  uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12;
 
   const uint8_t raw_type = raw[raw_offset];
   const uint8_t data_length = raw[raw_offset + 2];
@@ -101,7 +103,14 @@ optional<XiaomiParseResult> parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d
     return {};
   }
   XiaomiParseResult result;
-  result.type = is_miflora ? XiaomiParseResult::TYPE_MIFLORA : XiaomiParseResult::TYPE_MIJIA;
+  result.type = XiaomiParseResult::TYPE_HHCCJCY01;
+  if (is_lywsdcgq) {
+    result.type = XiaomiParseResult::TYPE_LYWSDCGQ;
+  } else if (is_lywsd02) {
+    result.type = XiaomiParseResult::TYPE_LYWSD02;
+  } else if (is_cgg1) {
+    result.type = XiaomiParseResult::TYPE_CGG1;
+  }
   bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result);
   if (!success)
     return {};
@@ -113,7 +122,14 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
   if (!res.has_value())
     return false;
 
-  const char *name = res->type == XiaomiParseResult::TYPE_MIFLORA ? "Mi Flora" : "Mi Jia";
+  const char *name = "HHCCJCY01";
+  if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) {
+    name = "LYWSDCGQ";
+  } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) {
+    name = "LYWSD02";
+  } else if (res->type == XiaomiParseResult::TYPE_CGG1) {
+    name = "CGG1";
+  }
 
   ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str());
 
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h
index 058a89927b..824ea80edf 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.h
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.h
@@ -9,7 +9,7 @@ namespace esphome {
 namespace xiaomi_ble {
 
 struct XiaomiParseResult {
-  enum { TYPE_MIJIA, TYPE_MIFLORA } type;
+  enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type;
   optional<float> temperature;
   optional<float> humidity;
   optional<float> battery_level;
diff --git a/esphome/components/xiaomi_cgg1/__init__.py b/esphome/components/xiaomi_cgg1/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py
new file mode 100644
index 0000000000..897687c68a
--- /dev/null
+++ b/esphome/components/xiaomi_cgg1/sensor.py
@@ -0,0 +1,38 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
+    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID
+
+DEPENDENCIES = ['esp32_ble_tracker']
+AUTO_LOAD = ['xiaomi_ble']
+
+xiaomi_cgg1_ns = cg.esphome_ns.namespace('xiaomi_cgg1')
+XiaomiCGG1 = xiaomi_cgg1_ns.class_(
+    'XiaomiCGG1', esp32_ble_tracker.ESPBTDeviceListener, cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(XiaomiCGG1),
+    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
+}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
+    if CONF_BATTERY_LEVEL in config:
+        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
similarity index 59%
rename from esphome/components/xiaomi_mijia/xiaomi_mijia.cpp
rename to esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
index 544af32d7b..6cc14f5a8e 100644
--- a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp
+++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp
@@ -1,21 +1,21 @@
-#include "xiaomi_mijia.h"
+#include "xiaomi_cgg1.h"
 #include "esphome/core/log.h"
 
 #ifdef ARDUINO_ARCH_ESP32
 
 namespace esphome {
-namespace xiaomi_mijia {
+namespace xiaomi_cgg1 {
 
-static const char *TAG = "xiaomi_mijia";
+static const char *TAG = "xiaomi_cgg1";
 
-void XiaomiMijia::dump_config() {
-  ESP_LOGCONFIG(TAG, "Xiaomi Mijia");
+void XiaomiCGG1::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi CGG1");
   LOG_SENSOR("  ", "Temperature", this->temperature_);
   LOG_SENSOR("  ", "Humidity", this->humidity_);
   LOG_SENSOR("  ", "Battery Level", this->battery_level_);
 }
 
-}  // namespace xiaomi_mijia
+}  // namespace xiaomi_cgg1
 }  // namespace esphome
 
 #endif
diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
similarity index 91%
rename from esphome/components/xiaomi_mijia/xiaomi_mijia.h
rename to esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
index 814e33fa75..7f73011275 100644
--- a/esphome/components/xiaomi_mijia/xiaomi_mijia.h
+++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
@@ -8,9 +8,9 @@
 #ifdef ARDUINO_ARCH_ESP32
 
 namespace esphome {
-namespace xiaomi_mijia {
+namespace xiaomi_cgg1 {
 
-class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
  public:
   void set_address(uint64_t address) { address_ = address; }
 
@@ -44,7 +44,7 @@ class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListe
   sensor::Sensor *battery_level_{nullptr};
 };
 
-}  // namespace xiaomi_mijia
+}  // namespace xiaomi_cgg1
 }  // namespace esphome
 
 #endif
diff --git a/esphome/components/xiaomi_hhccjcy01/__init__.py b/esphome/components/xiaomi_hhccjcy01/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py
new file mode 100644
index 0000000000..495446ba11
--- /dev/null
+++ b/esphome/components/xiaomi_hhccjcy01/sensor.py
@@ -0,0 +1,49 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
+    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \
+    CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \
+    UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER
+
+DEPENDENCIES = ['esp32_ble_tracker']
+AUTO_LOAD = ['xiaomi_ble']
+
+xiaomi_hhccjcy01_ns = cg.esphome_ns.namespace('xiaomi_hhccjcy01')
+XiaomiHHCCJCY01 = xiaomi_hhccjcy01_ns.class_('XiaomiHHCCJCY01',
+                                             esp32_ble_tracker.ESPBTDeviceListener, cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01),
+    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0),
+    cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0),
+    cv.Optional(CONF_CONDUCTIVITY):
+        sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0),
+    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
+}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_MOISTURE in config:
+        sens = yield sensor.new_sensor(config[CONF_MOISTURE])
+        cg.add(var.set_moisture(sens))
+    if CONF_ILLUMINANCE in config:
+        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
+        cg.add(var.set_illuminance(sens))
+    if CONF_CONDUCTIVITY in config:
+        sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        cg.add(var.set_conductivity(sens))
+    if CONF_BATTERY_LEVEL in config:
+        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp
similarity index 64%
rename from esphome/components/xiaomi_miflora/xiaomi_miflora.cpp
rename to esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp
index 966c78a1a6..8c8152c54c 100644
--- a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp
+++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp
@@ -1,15 +1,15 @@
-#include "xiaomi_miflora.h"
+#include "xiaomi_hhccjcy01.h"
 #include "esphome/core/log.h"
 
 #ifdef ARDUINO_ARCH_ESP32
 
 namespace esphome {
-namespace xiaomi_miflora {
+namespace xiaomi_hhccjcy01 {
 
-static const char *TAG = "xiaomi_miflora";
+static const char *TAG = "xiaomi_hhccjcy01";
 
-void XiaomiMiflora::dump_config() {
-  ESP_LOGCONFIG(TAG, "Xiaomi Mijia");
+void XiaomiHHCCJCY01::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi HHCCJCY01");
   LOG_SENSOR("  ", "Temperature", this->temperature_);
   LOG_SENSOR("  ", "Moisture", this->moisture_);
   LOG_SENSOR("  ", "Conductivity", this->conductivity_);
@@ -17,7 +17,7 @@ void XiaomiMiflora::dump_config() {
   LOG_SENSOR("  ", "Battery Level", this->battery_level_);
 }
 
-}  // namespace xiaomi_miflora
+}  // namespace xiaomi_hhccjcy01
 }  // namespace esphome
 
 #endif
diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
similarity index 93%
rename from esphome/components/xiaomi_miflora/xiaomi_miflora.h
rename to esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
index d1f05cdcc7..c1b8511bb8 100644
--- a/esphome/components/xiaomi_miflora/xiaomi_miflora.h
+++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
@@ -8,9 +8,9 @@
 #ifdef ARDUINO_ARCH_ESP32
 
 namespace esphome {
-namespace xiaomi_miflora {
+namespace xiaomi_hhccjcy01 {
 
-class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
  public:
   void set_address(uint64_t address) { address_ = address; }
 
@@ -52,7 +52,7 @@ class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceLis
   sensor::Sensor *battery_level_{nullptr};
 };
 
-}  // namespace xiaomi_miflora
+}  // namespace xiaomi_hhccjcy01
 }  // namespace esphome
 
 #endif
diff --git a/esphome/components/xiaomi_lywsd02/__init__.py b/esphome/components/xiaomi_lywsd02/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py
new file mode 100644
index 0000000000..8e4d59316b
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd02/sensor.py
@@ -0,0 +1,34 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
+    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID
+
+DEPENDENCIES = ['esp32_ble_tracker']
+AUTO_LOAD = ['xiaomi_ble']
+
+xiaomi_lywsd02_ns = cg.esphome_ns.namespace('xiaomi_lywsd02')
+XiaomiLYWSD02 = xiaomi_lywsd02_ns.class_('XiaomiLYWSD02', esp32_ble_tracker.ESPBTDeviceListener,
+                                         cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(XiaomiLYWSD02),
+    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
new file mode 100644
index 0000000000..cd77c133a5
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
@@ -0,0 +1,20 @@
+#include "xiaomi_lywsd02.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsd02 {
+
+static const char *TAG = "xiaomi_lywsd02";
+
+void XiaomiLYWSD02::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02");
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+}
+
+}  // namespace xiaomi_lywsd02
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
new file mode 100644
index 0000000000..9b8aba1bb0
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsd02 {
+
+class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; }
+
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
+    if (device.address_uint64() != this->address_)
+      return false;
+
+    auto res = xiaomi_ble::parse_xiaomi(device);
+    if (!res.has_value())
+      return false;
+
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    return true;
+  }
+
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
+  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+};
+
+}  // namespace xiaomi_lywsd02
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsdcgq/__init__.py b/esphome/components/xiaomi_lywsdcgq/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py
new file mode 100644
index 0000000000..e13c860464
--- /dev/null
+++ b/esphome/components/xiaomi_lywsdcgq/sensor.py
@@ -0,0 +1,38 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
+    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID
+
+DEPENDENCIES = ['esp32_ble_tracker']
+AUTO_LOAD = ['xiaomi_ble']
+
+xiaomi_lywsdcgq_ns = cg.esphome_ns.namespace('xiaomi_lywsdcgq')
+XiaomiLYWSDCGQ = xiaomi_lywsdcgq_ns.class_('XiaomiLYWSDCGQ', esp32_ble_tracker.ESPBTDeviceListener,
+                                           cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ),
+    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
+}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
+    if CONF_BATTERY_LEVEL in config:
+        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp
new file mode 100644
index 0000000000..2dacff2876
--- /dev/null
+++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp
@@ -0,0 +1,21 @@
+#include "xiaomi_lywsdcgq.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsdcgq {
+
+static const char *TAG = "xiaomi_lywsdcgq";
+
+void XiaomiLYWSDCGQ::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi LYWSDCGQ");
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+}  // namespace xiaomi_lywsdcgq
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h
new file mode 100644
index 0000000000..b6756eec61
--- /dev/null
+++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsdcgq {
+
+class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; }
+
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
+    if (device.address_uint64() != this->address_)
+      return false;
+
+    auto res = xiaomi_ble::parse_xiaomi(device);
+    if (!res.has_value())
+      return false;
+
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    return true;
+  }
+
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
+  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
+  void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_lywsdcgq
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py
index 8be06a93f3..0a0b3ff63f 100644
--- a/esphome/components/xiaomi_miflora/sensor.py
+++ b/esphome/components/xiaomi_miflora/sensor.py
@@ -1,49 +1,3 @@
-import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import sensor, esp32_ble_tracker
-from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
-    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \
-    CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \
-    UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER
 
-DEPENDENCIES = ['esp32_ble_tracker']
-AUTO_LOAD = ['xiaomi_ble']
-
-xiaomi_miflora_ns = cg.esphome_ns.namespace('xiaomi_miflora')
-XiaomiMiflora = xiaomi_miflora_ns.class_('XiaomiMiflora', esp32_ble_tracker.ESPBTDeviceListener,
-                                         cg.Component)
-
-CONFIG_SCHEMA = cv.Schema({
-    cv.GenerateID(): cv.declare_id(XiaomiMiflora),
-    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
-    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
-    cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0),
-    cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0),
-    cv.Optional(CONF_CONDUCTIVITY):
-        sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0),
-    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
-
-
-def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-
-    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
-
-    if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
-        cg.add(var.set_temperature(sens))
-    if CONF_MOISTURE in config:
-        sens = yield sensor.new_sensor(config[CONF_MOISTURE])
-        cg.add(var.set_moisture(sens))
-    if CONF_ILLUMINANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
-        cg.add(var.set_illuminance(sens))
-    if CONF_CONDUCTIVITY in config:
-        sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY])
-        cg.add(var.set_conductivity(sens))
-    if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
-        cg.add(var.set_battery_level(sens))
+CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_hhccjcy01")
diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py
index 995a6cbf25..597d8d1bce 100644
--- a/esphome/components/xiaomi_mijia/sensor.py
+++ b/esphome/components/xiaomi_mijia/sensor.py
@@ -1,38 +1,3 @@
-import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import sensor, esp32_ble_tracker
-from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
-    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID
 
-DEPENDENCIES = ['esp32_ble_tracker']
-AUTO_LOAD = ['xiaomi_ble']
-
-xiaomi_mijia_ns = cg.esphome_ns.namespace('xiaomi_mijia')
-XiaomiMijia = xiaomi_mijia_ns.class_('XiaomiMijia', esp32_ble_tracker.ESPBTDeviceListener,
-                                     cg.Component)
-
-CONFIG_SCHEMA = cv.Schema({
-    cv.GenerateID(): cv.declare_id(XiaomiMijia),
-    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
-    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
-    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
-    cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0),
-}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
-
-
-def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-
-    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
-
-    if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
-        cg.add(var.set_temperature(sens))
-    if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
-        cg.add(var.set_humidity(sens))
-    if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
-        cg.add(var.set_battery_level(sens))
+CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_lywsdcgq")
diff --git a/esphome/components/yashima/__init__.py b/esphome/components/yashima/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py
new file mode 100644
index 0000000000..4c4b98d9e7
--- /dev/null
+++ b/esphome/components/yashima/climate.py
@@ -0,0 +1,33 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import climate, remote_transmitter, sensor
+from esphome.components.remote_base import CONF_TRANSMITTER_ID
+from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
+
+AUTO_LOAD = ['sensor']
+
+yashima_ns = cg.esphome_ns.namespace('yashima')
+YashimaClimate = yashima_ns.class_('YashimaClimate', climate.Climate, cg.Component)
+
+CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(YashimaClimate),
+    cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
+    cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
+    cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
+    cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
+}).extend(cv.COMPONENT_SCHEMA))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield climate.register_climate(var, config)
+
+    cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
+    cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
+    if CONF_SENSOR in config:
+        sens = yield cg.get_variable(config[CONF_SENSOR])
+        cg.add(var.set_sensor(sens))
+
+    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
+    cg.add(var.set_transmitter(transmitter))
diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp
new file mode 100644
index 0000000000..e3c0a33127
--- /dev/null
+++ b/esphome/components/yashima/yashima.cpp
@@ -0,0 +1,193 @@
+#include "yashima.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace yashima {
+
+static const char *TAG = "yashima.climate";
+
+const uint16_t YASHIMA_STATE_LENGTH = 9;
+const uint16_t YASHIMA_BITS = YASHIMA_STATE_LENGTH * 8;
+
+/* the bit masks are intended to be sent from the MSB to the LSB */
+const uint8_t YASHIMA_MODE_HEAT_BYTE0 = 0b00100000;
+const uint8_t YASHIMA_MODE_DRY_BYTE0 = 0b01100000;
+const uint8_t YASHIMA_MODE_COOL_BYTE0 = 0b11100000;
+const uint8_t YASHIMA_MODE_FAN_BYTE0 = 0b10100000;
+const uint8_t YASHIMA_MODE_AUTO_BYTE0 = 0b11100000;
+const uint8_t YASHIMA_MODE_OFF_BYTE0 = 0b11110000;
+const uint8_t YASHIMA_BASE_BYTE0 = 0b1110;
+
+const uint8_t YASHIMA_TEMP_MAX = 30;  // Celsius
+const uint8_t YASHIMA_TEMP_MIN = 16;  // Celsius
+const uint8_t YASHIMA_TEMP_RANGE = YASHIMA_TEMP_MAX - YASHIMA_TEMP_MIN + 1;
+
+const uint8_t YASHIMA_TEMP_MAP_BYTE1[YASHIMA_TEMP_RANGE] = {
+    0b01100100,  // 16C
+    0b10100100,  // 17C
+    0b00100100,  // 18C
+    0b11000100,  // 19C
+    0b01000100,  // 20C
+    0b10000100,  // 21C
+    0b00000100,  // 22C
+    0b11111000,  // 23C
+    0b01111000,  // 24C
+    0b10111000,  // 25C
+    0b00111000,  // 26C
+    0b11011000,  // 27C
+    0b01011000,  // 28C
+    0b10011000,  // 29C
+    0b00011000,  // 30C
+};
+const uint8_t YASHIMA_BASE_BYTE1 = 0b11;
+
+const uint8_t YASHIMA_FAN_AUTO_BYTE2 = 0b11000000;
+const uint8_t YASHIMA_FAN_LOW_BYTE2 = 0b00000000;
+const uint8_t YASHIMA_FAN_MEDIUM_BYTE2 = 0b10000000;
+const uint8_t YASHIMA_FAN_HIGH_BYTE2 = 0b01000000;
+const uint8_t YASHIMA_BASE_BYTE2 = 0b111111;
+
+const uint8_t YASHIMA_BASE_BYTE3 = 0b11111111;
+const uint8_t YASHIMA_BASE_BYTE4 = 0b11;
+
+const uint8_t YASHIMA_MODE_HEAT_BYTE5 = 0b00000000;
+const uint8_t YASHIMA_MODE_DRY_BYTE5 = 0b00000000;
+const uint8_t YASHIMA_MODE_FAN_BYTE5 = 0b00000000;
+const uint8_t YASHIMA_MODE_AUTO_BYTE5 = 0b00000000;
+const uint8_t YASHIMA_MODE_COOL_BYTE5 = 0b10000000;
+const uint8_t YASHIMA_MODE_OFF_BYTE5 = 0b10000000;
+const uint8_t YASHIMA_BASE_BYTE5 = 0b11111;
+
+const uint8_t YASHIMA_BASE_BYTE6 = 0b11111111;
+const uint8_t YASHIMA_BASE_BYTE7 = 0b11111111;
+const uint8_t YASHIMA_BASE_BYTE8 = 0b11001111;
+
+/* values sampled using a Broadlink Mini 3: */
+// const uint16_t YASHIMA_HEADER_MARK = 9600;
+// const uint16_t YASHIMA_HEADER_SPACE = 4800;
+// const uint16_t YASHIMA_BIT_MARK = 720;
+// const uint16_t YASHIMA_ONE_SPACE = 550;
+// const uint16_t YASHIMA_ZERO_SPACE = 1640;
+
+/* scaled values to get correct timing on ESP8266/ESP32: */
+const uint16_t YASHIMA_HEADER_MARK = 9035;
+const uint16_t YASHIMA_HEADER_SPACE = 4517;
+const uint16_t YASHIMA_BIT_MARK = 667;
+const uint16_t YASHIMA_ONE_SPACE = 517;
+const uint16_t YASHIMA_ZERO_SPACE = 1543;
+const uint32_t YASHIMA_GAP = YASHIMA_HEADER_SPACE;
+
+const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000;
+
+climate::ClimateTraits YashimaClimate::traits() {
+  auto traits = climate::ClimateTraits();
+  traits.set_supports_current_temperature(this->sensor_ != nullptr);
+  traits.set_supports_auto_mode(true);
+  traits.set_supports_cool_mode(this->supports_cool_);
+  traits.set_supports_heat_mode(this->supports_heat_);
+  traits.set_supports_two_point_target_temperature(false);
+  traits.set_supports_away(false);
+  traits.set_visual_min_temperature(YASHIMA_TEMP_MIN);
+  traits.set_visual_max_temperature(YASHIMA_TEMP_MAX);
+  traits.set_visual_temperature_step(1);
+  return traits;
+}
+
+void YashimaClimate::setup() {
+  if (this->sensor_) {
+    this->sensor_->add_on_state_callback([this](float state) {
+      this->current_temperature = state;
+      // current temperature changed, publish state
+      this->publish_state();
+    });
+    this->current_temperature = this->sensor_->state;
+  } else
+    this->current_temperature = NAN;
+  // restore set points
+  auto restore = this->restore_state_();
+  if (restore.has_value()) {
+    restore->apply(this);
+  } else {
+    // restore from defaults
+    this->mode = climate::CLIMATE_MODE_OFF;
+    this->target_temperature = 24;
+  }
+}
+
+void YashimaClimate::control(const climate::ClimateCall &call) {
+  if (call.get_mode().has_value())
+    this->mode = *call.get_mode();
+  if (call.get_target_temperature().has_value())
+    this->target_temperature = *call.get_target_temperature();
+
+  this->transmit_state_();
+  this->publish_state();
+}
+
+void YashimaClimate::transmit_state_() {
+  uint8_t remote_state[YASHIMA_STATE_LENGTH] = {0};
+
+  remote_state[0] = YASHIMA_BASE_BYTE0;
+  remote_state[1] = YASHIMA_BASE_BYTE1;
+  remote_state[2] = YASHIMA_BASE_BYTE2;
+  remote_state[3] = YASHIMA_BASE_BYTE3;
+  remote_state[4] = YASHIMA_BASE_BYTE4;
+  remote_state[5] = YASHIMA_BASE_BYTE5;
+  remote_state[6] = YASHIMA_BASE_BYTE6;
+  remote_state[7] = YASHIMA_BASE_BYTE7;
+  remote_state[8] = YASHIMA_BASE_BYTE8;
+
+  // Set mode
+  switch (this->mode) {
+    case climate::CLIMATE_MODE_AUTO:
+      remote_state[0] |= YASHIMA_MODE_AUTO_BYTE0;
+      remote_state[5] |= YASHIMA_MODE_AUTO_BYTE5;
+      break;
+    case climate::CLIMATE_MODE_COOL:
+      remote_state[0] |= YASHIMA_MODE_COOL_BYTE0;
+      remote_state[5] |= YASHIMA_MODE_COOL_BYTE5;
+      break;
+    case climate::CLIMATE_MODE_HEAT:
+      remote_state[0] |= YASHIMA_MODE_HEAT_BYTE0;
+      remote_state[5] |= YASHIMA_MODE_HEAT_BYTE5;
+      break;
+    case climate::CLIMATE_MODE_OFF:
+    default:
+      remote_state[0] |= YASHIMA_MODE_OFF_BYTE0;
+      remote_state[5] |= YASHIMA_MODE_OFF_BYTE5;
+      break;
+      // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY are missing in esphome
+  }
+
+  // TODO: missing support for fan speed
+  remote_state[2] |= YASHIMA_FAN_AUTO_BYTE2;
+
+  // Set temperature
+  uint8_t safecelsius = std::max((uint8_t) this->target_temperature, YASHIMA_TEMP_MIN);
+  safecelsius = std::min(safecelsius, YASHIMA_TEMP_MAX);
+  remote_state[1] |= YASHIMA_TEMP_MAP_BYTE1[safecelsius - YASHIMA_TEMP_MIN];
+
+  auto transmit = this->transmitter_->transmit();
+  auto data = transmit.get_data();
+
+  data->set_carrier_frequency(YASHIMA_CARRIER_FREQUENCY);
+
+  // Header
+  data->mark(YASHIMA_HEADER_MARK);
+  data->space(YASHIMA_HEADER_SPACE);
+  // Data (sent from the MSB to the LSB)
+  for (uint8_t i : remote_state)
+    for (int8_t j = 7; j >= 0; j--) {
+      data->mark(YASHIMA_BIT_MARK);
+      bool bit = i & (1 << j);
+      data->space(bit ? YASHIMA_ONE_SPACE : YASHIMA_ZERO_SPACE);
+    }
+  // Footer
+  data->mark(YASHIMA_BIT_MARK);
+  data->space(YASHIMA_GAP);
+
+  transmit.perform();
+}
+
+}  // namespace yashima
+}  // namespace esphome
diff --git a/esphome/components/yashima/yashima.h b/esphome/components/yashima/yashima.h
new file mode 100644
index 0000000000..466816bd5f
--- /dev/null
+++ b/esphome/components/yashima/yashima.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/climate/climate.h"
+#include "esphome/components/remote_base/remote_base.h"
+#include "esphome/components/remote_transmitter/remote_transmitter.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace yashima {
+
+class YashimaClimate : public climate::Climate, public Component {
+ public:
+  void setup() override;
+  void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
+    this->transmitter_ = transmitter;
+  }
+  void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
+  void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
+  void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
+
+ 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;
+
+  /// Transmit via IR the state of this climate controller.
+  void transmit_state_();
+
+  bool supports_cool_{true};
+  bool supports_heat_{true};
+
+  remote_transmitter::RemoteTransmitterComponent *transmitter_;
+  sensor::Sensor *sensor_{nullptr};
+};
+
+}  // namespace yashima
+}  // namespace esphome
diff --git a/esphome/components/zyaura/__init__.py b/esphome/components/zyaura/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py
new file mode 100644
index 0000000000..649b80b444
--- /dev/null
+++ b/esphome/components/zyaura/sensor.py
@@ -0,0 +1,43 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import sensor
+from esphome.const import CONF_ID, CONF_CLOCK_PIN, CONF_DATA_PIN, \
+    CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, \
+    UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, UNIT_PERCENT, \
+    ICON_PERIODIC_TABLE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT
+from esphome.cpp_helpers import gpio_pin_expression
+
+zyaura_ns = cg.esphome_ns.namespace('zyaura')
+ZyAuraSensor = zyaura_ns.class_('ZyAuraSensor', cg.PollingComponent)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(ZyAuraSensor),
+    cv.Required(CONF_CLOCK_PIN): cv.All(pins.internal_gpio_input_pin_schema,
+                                        pins.validate_has_interrupt),
+    cv.Required(CONF_DATA_PIN): cv.All(pins.internal_gpio_input_pin_schema,
+                                       pins.validate_has_interrupt),
+    cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0),
+    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+}).extend(cv.polling_component_schema('60s'))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+
+    pin_clock = yield gpio_pin_expression(config[CONF_CLOCK_PIN])
+    cg.add(var.set_pin_clock(pin_clock))
+    pin_data = yield gpio_pin_expression(config[CONF_DATA_PIN])
+    cg.add(var.set_pin_data(pin_data))
+
+    if CONF_CO2 in config:
+        sens = yield sensor.new_sensor(config[CONF_CO2])
+        cg.add(var.set_co2_sensor(sens))
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature_sensor(sens))
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp
new file mode 100644
index 0000000000..3b1a2a5069
--- /dev/null
+++ b/esphome/components/zyaura/zyaura.cpp
@@ -0,0 +1,117 @@
+#include "zyaura.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace zyaura {
+
+static const char *TAG = "zyaura";
+
+bool ICACHE_RAM_ATTR ZaDataProcessor::decode(unsigned long ms, bool data) {
+  // check if a new message has started, based on time since previous bit
+  if ((ms - this->prev_ms_) > ZA_MAX_MS) {
+    this->num_bits_ = 0;
+  }
+  this->prev_ms_ = ms;
+
+  // number of bits received is basically the "state"
+  if (this->num_bits_ < ZA_FRAME_SIZE) {
+    // store it while it fits
+    int idx = this->num_bits_ / 8;
+    this->buffer_[idx] = (this->buffer_[idx] << 1) | (data ? 1 : 0);
+    this->num_bits_++;
+
+    // are we done yet?
+    if (this->num_bits_ == ZA_FRAME_SIZE) {
+      // validate checksum
+      uint8_t checksum = this->buffer_[ZA_BYTE_TYPE] + this->buffer_[ZA_BYTE_HIGH] + this->buffer_[ZA_BYTE_LOW];
+      if (checksum != this->buffer_[ZA_BYTE_SUM] || this->buffer_[ZA_BYTE_END] != ZA_MSG_DELIMETER) {
+        return false;
+      }
+
+      this->message->type = (ZaDataType) this->buffer_[ZA_BYTE_TYPE];
+      this->message->value = this->buffer_[ZA_BYTE_HIGH] << 8 | this->buffer_[ZA_BYTE_LOW];
+      return true;
+    }
+  }
+
+  return false;
+}
+
+void ZaSensorStore::setup(GPIOPin *pin_clock, GPIOPin *pin_data) {
+  pin_clock->setup();
+  pin_data->setup();
+  this->pin_clock_ = pin_clock->to_isr();
+  this->pin_data_ = pin_data->to_isr();
+  pin_clock->attach_interrupt(ZaSensorStore::interrupt, this, FALLING);
+}
+
+void ICACHE_RAM_ATTR ZaSensorStore::interrupt(ZaSensorStore *arg) {
+  uint32_t now = millis();
+  bool data_bit = arg->pin_data_->digital_read();
+
+  if (arg->processor_.decode(now, data_bit)) {
+    arg->set_data_(arg->processor_.message);
+  }
+}
+
+void ICACHE_RAM_ATTR ZaSensorStore::set_data_(ZaMessage *message) {
+  switch (message->type) {
+    case HUMIDITY:
+      this->humidity = (message->value > 10000) ? NAN : (message->value / 100.0f);
+      break;
+
+    case TEMPERATURE:
+      this->temperature = (message->value > 5970) ? NAN : (message->value / 16.0f - 273.15f);
+      break;
+
+    case CO2:
+      this->co2 = (message->value > 10000) ? NAN : message->value;
+      break;
+
+    default:
+      break;
+  }
+}
+
+bool ZyAuraSensor::publish_state_(sensor::Sensor *sensor, float *value) {
+  // Sensor doesn't added to configuration
+  if (sensor == nullptr) {
+    return true;
+  }
+
+  sensor->publish_state(*value);
+
+  // Sensor reported wrong value
+  if (isnan(*value)) {
+    ESP_LOGW(TAG, "Sensor reported invalid data. Is the update interval too small?");
+    this->status_set_warning();
+    return false;
+  }
+
+  *value = NAN;
+  return true;
+}
+
+void ZyAuraSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "ZyAuraSensor:");
+  LOG_PIN("  Pin Clock: ", this->pin_clock_);
+  LOG_PIN("  Pin Data: ", this->pin_data_);
+  LOG_UPDATE_INTERVAL(this);
+
+  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
+}
+
+void ZyAuraSensor::update() {
+  bool co2_result = this->publish_state_(this->co2_sensor_, &this->store_.co2);
+  bool temperature_result = this->publish_state_(this->temperature_sensor_, &this->store_.temperature);
+  bool humidity_result = this->publish_state_(this->humidity_sensor_, &this->store_.humidity);
+
+  if (co2_result && temperature_result && humidity_result) {
+    this->status_clear_warning();
+  }
+}
+
+}  // namespace zyaura
+}  // namespace esphome
diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h
new file mode 100644
index 0000000000..fd26947e28
--- /dev/null
+++ b/esphome/components/zyaura/zyaura.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/esphal.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace zyaura {
+
+static const uint8_t ZA_MAX_MS = 2;
+static const uint8_t ZA_MSG_LEN = 5;
+static const uint8_t ZA_FRAME_SIZE = 40;
+static const uint8_t ZA_MSG_DELIMETER = 0x0D;
+
+static const uint8_t ZA_BYTE_TYPE = 0;
+static const uint8_t ZA_BYTE_HIGH = 1;
+static const uint8_t ZA_BYTE_LOW = 2;
+static const uint8_t ZA_BYTE_SUM = 3;
+static const uint8_t ZA_BYTE_END = 4;
+
+enum ZaDataType {
+  HUMIDITY = 0x41,
+  TEMPERATURE = 0x42,
+  CO2 = 0x50,
+};
+
+struct ZaMessage {
+  ZaDataType type;
+  uint16_t value;
+};
+
+class ZaDataProcessor {
+ public:
+  bool decode(unsigned long ms, bool data);
+  ZaMessage *message = new ZaMessage;
+
+ protected:
+  uint8_t buffer_[ZA_MSG_LEN];
+  int num_bits_ = 0;
+  unsigned long prev_ms_;
+};
+
+class ZaSensorStore {
+ public:
+  float co2 = NAN;
+  float temperature = NAN;
+  float humidity = NAN;
+
+  void setup(GPIOPin *pin_clock, GPIOPin *pin_data);
+  static void interrupt(ZaSensorStore *arg);
+
+ protected:
+  ISRInternalGPIOPin *pin_clock_;
+  ISRInternalGPIOPin *pin_data_;
+  ZaDataProcessor processor_;
+
+  void set_data_(ZaMessage *message);
+};
+
+/// Component for reading temperature/co2/humidity measurements from ZyAura sensors.
+class ZyAuraSensor : public PollingComponent {
+ public:
+  void set_pin_clock(GPIOPin *pin) { pin_clock_ = pin; }
+  void set_pin_data(GPIOPin *pin) { pin_data_ = pin; }
+  void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
+  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
+
+  void setup() override { this->store_.setup(this->pin_clock_, this->pin_data_); }
+  void dump_config() override;
+  void update() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  ZaSensorStore store_;
+  GPIOPin *pin_clock_;
+  GPIOPin *pin_data_;
+  sensor::Sensor *co2_sensor_{nullptr};
+  sensor::Sensor *temperature_sensor_{nullptr};
+  sensor::Sensor *humidity_sensor_{nullptr};
+
+  bool publish_state_(sensor::Sensor *sensor, float *value);
+};
+
+}  // namespace zyaura
+}  // namespace esphome
diff --git a/esphome/config.py b/esphome/config.py
index b5075e5e84..5906e2fc95 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -18,7 +18,7 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS
 from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS
 from esphome.core import CORE, EsphomeError  # noqa
 from esphome.helpers import color, indent
-from esphome.py_compat import text_type, IS_PY2
+from esphome.py_compat import text_type, IS_PY2, decode_text
 from esphome.util import safe_print, OrderedDict
 
 from typing import List, Optional, Tuple, Union  # noqa
@@ -634,8 +634,13 @@ def _format_vol_invalid(ex, config):
 
 class InvalidYAMLError(EsphomeError):
     def __init__(self, base_exc):
+        try:
+            base = str(base_exc)
+        except UnicodeDecodeError:
+            base = repr(base_exc)
+        base = decode_text(base)
         message = u"Invalid YAML syntax. Please see YAML syntax reference or use an " \
-                  u"online YAML syntax validator:\n\n{}".format(base_exc)
+                  u"online YAML syntax validator:\n\n{}".format(base)
         super(InvalidYAMLError, self).__init__(message)
         self.base_exc = base_exc
 
@@ -797,15 +802,15 @@ def strip_default_ids(config):
     return config
 
 
-def read_config(verbose):
-    _LOGGER.info("Reading configuration...")
+def read_config():
+    _LOGGER.info("Reading configuration %s...", CORE.config_path)
     try:
         res = load_config()
     except EsphomeError as err:
         _LOGGER.error(u"Error while reading config: %s", err)
         return None
     if res.errors:
-        if not verbose:
+        if not CORE.verbose:
             res = strip_default_ids(res)
 
         safe_print(color('bold_red', u"Failed config"))
diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py
index ddad36f8a8..0c508d2202 100644
--- a/esphome/config_helpers.py
+++ b/esphome/config_helpers.py
@@ -1,10 +1,10 @@
 from __future__ import print_function
 
-import codecs
 import json
 import os
 
-from esphome.core import CORE, EsphomeError
+from esphome.core import CORE
+from esphome.helpers import read_file
 from esphome.py_compat import safe_input
 
 
@@ -20,10 +20,4 @@ def read_config_file(path):
         assert data['type'] == 'file_response'
         return data['content']
 
-    try:
-        with codecs.open(path, encoding='utf-8') as handle:
-            return handle.read()
-    except IOError as exc:
-        raise EsphomeError(u"Error accessing file {}: {}".format(path, exc))
-    except UnicodeDecodeError as exc:
-        raise EsphomeError(u"Unable to read file {}: {}".format(path, exc))
+    return read_file(path)
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index 4ec3033452..956779f655 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -61,7 +61,7 @@ RESERVED_IDS = [
     'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT',
     'OUTPUT',
     'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t',
-    'close', 'pause', 'sleep', 'open',
+    'close', 'pause', 'sleep', 'open', 'setup', 'loop',
 ]
 
 
@@ -80,6 +80,7 @@ class Optional(vol.Optional):
     during config validation - specifically *not* in the C++ code or the code generation
     phase.
     """
+
     def __init__(self, key, default=UNDEFINED):
         super(Optional, self).__init__(key, default=default)
 
@@ -91,6 +92,7 @@ class Required(vol.Required):
     All required values should be acceessed with the `config[CONF_<KEY>]` syntax in code
     - *not* the `config.get(CONF_<KEY>)` syntax.
     """
+
     def __init__(self, key):
         super(Required, self).__init__(key)
 
@@ -459,6 +461,8 @@ def time_period_str_unit(value):
     if isinstance(value, int):
         raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean "
                       "'{0}s'?".format(value))
+    if isinstance(value, TimePeriod):
+        value = str(value)
     if not isinstance(value, string_types):
         raise Invalid("Expected string for time period with unit.")
 
@@ -1021,8 +1025,24 @@ def dimensions(value):
 
 
 def directory(value):
+    import json
+    from esphome.py_compat import safe_input
     value = string(value)
     path = CORE.relative_config_path(value)
+
+    if CORE.vscode and (not CORE.ace or
+                        os.path.abspath(path) == os.path.abspath(CORE.config_path)):
+        print(json.dumps({
+            'type': 'check_directory_exists',
+            'path': path,
+        }))
+        data = json.loads(safe_input())
+        assert data['type'] == 'directory_exists_response'
+        if data['content']:
+            return value
+        raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})."
+                      u"".format(path, os.path.abspath(path)))
+
     if not os.path.exists(path):
         raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})."
                       u"".format(path, os.path.abspath(path)))
@@ -1033,8 +1053,24 @@ def directory(value):
 
 
 def file_(value):
+    import json
+    from esphome.py_compat import safe_input
     value = string(value)
     path = CORE.relative_config_path(value)
+
+    if CORE.vscode and (not CORE.ace or
+                        os.path.abspath(path) == os.path.abspath(CORE.config_path)):
+        print(json.dumps({
+            'type': 'check_file_exists',
+            'path': path,
+        }))
+        data = json.loads(safe_input())
+        assert data['type'] == 'file_exists_response'
+        if data['content']:
+            return value
+        raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})."
+                      u"".format(path, os.path.abspath(path)))
+
     if not os.path.exists(path):
         raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})."
                       u"".format(path, os.path.abspath(path)))
@@ -1100,12 +1136,14 @@ def typed_schema(schemas, **kwargs):
 
 class GenerateID(Optional):
     """Mark this key as being an auto-generated ID key."""
+
     def __init__(self, key=CONF_ID):
         super(GenerateID, self).__init__(key, default=lambda: None)
 
 
 class SplitDefault(Optional):
     """Mark this key to have a split default for ESP8266/ESP32."""
+
     def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED):
         super(SplitDefault, self).__init__(key)
         self._esp8266_default = vol.default_factory(esp8266)
@@ -1127,6 +1165,7 @@ class SplitDefault(Optional):
 
 class OnlyWith(Optional):
     """Set the default value only if the given component is loaded."""
+
     def __init__(self, key, component, default=None):
         super(OnlyWith, self).__init__(key)
         self._component = component
@@ -1191,6 +1230,8 @@ def validate_registry_entry(name, registry):
 
         registry_entry = registry[key]
 
+        value = value.copy()
+
         with prepend_path([key]):
             value[key] = registry_entry.schema(value[key])
 
diff --git a/esphome/const.py b/esphome/const.py
index 097e00672b..137785ae02 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -2,8 +2,8 @@
 """Constants used by esphome."""
 
 MAJOR_VERSION = 1
-MINOR_VERSION = 13
-PATCH_VERSION = '6'
+MINOR_VERSION = 14
+PATCH_VERSION = '0b5'
 __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
 __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
 
@@ -15,11 +15,14 @@ ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
 ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage'
 ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0'
 ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0'
+ARDUINO_VERSION_ESP32_1_0_2 = 'espressif32@1.9.0'
+ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0'
+ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0'
 ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \
                               '/stage'
 ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0'
 ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0'
-ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.0'
+ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3'
 ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0'
 SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'}
 HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'}
@@ -38,6 +41,7 @@ CONF_ARGS = 'args'
 CONF_ASSUMED_STATE = 'assumed_state'
 CONF_AT = 'at'
 CONF_ATTENUATION = 'attenuation'
+CONF_AUTH = 'auth'
 CONF_AUTOMATION_ID = 'automation_id'
 CONF_AVAILABILITY = 'availability'
 CONF_AWAY = 'away'
@@ -59,10 +63,11 @@ CONF_BROKER = 'broker'
 CONF_BSSID = 'bssid'
 CONF_BUFFER_SIZE = 'buffer_size'
 CONF_BUILD_PATH = 'build_path'
-CONF_BUSY_PIN = 'busy_pin'
 CONF_BUS_VOLTAGE = 'bus_voltage'
+CONF_BUSY_PIN = 'busy_pin'
 CONF_CALIBRATE_LINEAR = 'calibrate_linear'
 CONF_CALIBRATION = 'calibration'
+CONF_CAPACITANCE = 'capacitance'
 CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent'
 CONF_CARRIER_FREQUENCY = 'carrier_frequency'
 CONF_CHANGE_MODE_EVERY = 'change_mode_every'
@@ -79,22 +84,23 @@ CONF_CO2 = 'co2'
 CONF_CODE = 'code'
 CONF_COLD_WHITE = 'cold_white'
 CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature'
-CONF_COLORS = 'colors'
 CONF_COLOR_CORRECT = 'color_correct'
 CONF_COLOR_TEMPERATURE = 'color_temperature'
+CONF_COLORS = 'colors'
 CONF_COMMAND = 'command'
 CONF_COMMAND_TOPIC = 'command_topic'
+CONF_COMMENT = 'comment'
 CONF_COMMIT = 'commit'
-CONF_COMPONENTS = 'components'
 CONF_COMPONENT_ID = 'component_id'
+CONF_COMPONENTS = 'components'
 CONF_CONDITION = 'condition'
 CONF_CONDITION_ID = 'condition_id'
 CONF_CONDUCTIVITY = 'conductivity'
 CONF_COOL_ACTION = 'cool_action'
 CONF_COUNT_MODE = 'count_mode'
 CONF_CRON = 'cron'
-CONF_CSS_URL = 'css_url'
 CONF_CS_PIN = 'cs_pin'
+CONF_CSS_URL = 'css_url'
 CONF_CURRENT = 'current'
 CONF_CURRENT_OPERATION = 'current_operation'
 CONF_CURRENT_RESISTOR = 'current_resistor'
@@ -116,11 +122,13 @@ CONF_DELTA = 'delta'
 CONF_DEVICE = 'device'
 CONF_DEVICE_CLASS = 'device_class'
 CONF_DIMENSIONS = 'dimensions'
-CONF_DIRECTION = 'direction'
 CONF_DIR_PIN = 'dir_pin'
+CONF_DIRECTION = 'direction'
 CONF_DISCOVERY = 'discovery'
 CONF_DISCOVERY_PREFIX = 'discovery_prefix'
 CONF_DISCOVERY_RETAIN = 'discovery_retain'
+CONF_DISTANCE = 'distance'
+CONF_DIV_RATIO = 'div_ratio'
 CONF_DNS1 = 'dns1'
 CONF_DNS2 = 'dns2'
 CONF_DOMAIN = 'domain'
@@ -136,6 +144,7 @@ CONF_ENTITY_ID = 'entity_id'
 CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash'
 CONF_ESPHOME = 'esphome'
 CONF_ESPHOME_CORE_VERSION = 'esphome_core_version'
+CONF_EVENT = 'event'
 CONF_EXPIRE_AFTER = 'expire_after'
 CONF_EXTERNAL_VCC = 'external_vcc'
 CONF_FALLING_EDGE = 'falling_edge'
@@ -143,10 +152,11 @@ CONF_FAMILY = 'family'
 CONF_FAST_CONNECT = 'fast_connect'
 CONF_FILE = 'file'
 CONF_FILTER = 'filter'
-CONF_FILTERS = 'filters'
 CONF_FILTER_OUT = 'filter_out'
+CONF_FILTERS = 'filters'
 CONF_FLASH_LENGTH = 'flash_length'
 CONF_FOR = 'for'
+CONF_FORCE_UPDATE = 'force_update'
 CONF_FORMALDEHYDE = 'formaldehyde'
 CONF_FORMAT = 'format'
 CONF_FREQUENCY = 'frequency'
@@ -162,8 +172,8 @@ CONF_GREEN = 'green'
 CONF_GROUP = 'group'
 CONF_HARDWARE_UART = 'hardware_uart'
 CONF_HEARTBEAT = 'heartbeat'
-CONF_HEATER = 'heater'
 CONF_HEAT_ACTION = 'heat_action'
+CONF_HEATER = 'heater'
 CONF_HIDDEN = 'hidden'
 CONF_HIGH = 'high'
 CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference'
@@ -182,6 +192,8 @@ CONF_IIR_FILTER = 'iir_filter'
 CONF_ILLUMINANCE = 'illuminance'
 CONF_INCLUDES = 'includes'
 CONF_INDEX = 'index'
+CONF_INDOOR = 'indoor'
+CONF_INITIAL_MODE = 'initial_mode'
 CONF_INITIAL_VALUE = 'initial_value'
 CONF_INTEGRATION_TIME = 'integration_time'
 CONF_INTENSITY = 'intensity'
@@ -195,23 +207,26 @@ CONF_INVERTED = 'inverted'
 CONF_IP_ADDRESS = 'ip_address'
 CONF_JS_URL = 'js_url'
 CONF_JVC = 'jvc'
-CONF_KEEPALIVE = 'keepalive'
 CONF_KEEP_ON_TIME = 'keep_on_time'
+CONF_KEEPALIVE = 'keepalive'
 CONF_LAMBDA = 'lambda'
 CONF_LEVEL = 'level'
 CONF_LG = 'lg'
 CONF_LIBRARIES = 'libraries'
 CONF_LIGHT = 'light'
+CONF_LIGHTNING_ENERGY = 'lightning_energy'
+CONF_LIGHTNING_THRESHOLD = 'lightning_threshold'
 CONF_LOADED_INTEGRATIONS = 'loaded_integrations'
 CONF_LOCAL = 'local'
+CONF_LOG_TOPIC = 'log_topic'
 CONF_LOGGER = 'logger'
 CONF_LOGS = 'logs'
-CONF_LOG_TOPIC = 'log_topic'
 CONF_LOW = 'low'
 CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference'
 CONF_MAC_ADDRESS = 'mac_address'
 CONF_MAKE_ID = 'make_id'
 CONF_MANUAL_IP = 'manual_ip'
+CONF_MASK_DISTURBER = 'mask_disturber'
 CONF_MAX_CURRENT = 'max_current'
 CONF_MAX_DURATION = 'max_duration'
 CONF_MAX_LENGTH = 'max_length'
@@ -225,13 +240,13 @@ CONF_MAX_VOLTAGE = 'max_voltage'
 CONF_MEASUREMENT_DURATION = 'measurement_duration'
 CONF_MEDIUM = 'medium'
 CONF_METHOD = 'method'
-CONF_MINUTE = 'minute'
-CONF_MINUTES = 'minutes'
 CONF_MIN_LENGTH = 'min_length'
 CONF_MIN_LEVEL = 'min_level'
 CONF_MIN_POWER = 'min_power'
 CONF_MIN_TEMPERATURE = 'min_temperature'
 CONF_MIN_VALUE = 'min_value'
+CONF_MINUTE = 'minute'
+CONF_MINUTES = 'minutes'
 CONF_MISO_PIN = 'miso_pin'
 CONF_MODE = 'mode'
 CONF_MODEL = 'model'
@@ -246,14 +261,14 @@ CONF_NAME = 'name'
 CONF_NBITS = 'nbits'
 CONF_NEC = 'nec'
 CONF_NETWORKS = 'networks'
-CONF_NUMBER = 'number'
+CONF_NOISE_LEVEL = 'noise_level'
 CONF_NUM_ATTEMPTS = 'num_attempts'
 CONF_NUM_CHANNELS = 'num_channels'
 CONF_NUM_CHIPS = 'num_chips'
 CONF_NUM_LEDS = 'num_leds'
+CONF_NUMBER = 'number'
 CONF_OFFSET = 'offset'
 CONF_ON = 'on'
-CONF_ONE = 'one'
 CONF_ON_BOOT = 'on_boot'
 CONF_ON_CLICK = 'on_click'
 CONF_ON_DOUBLE_CLICK = 'on_double_click'
@@ -272,6 +287,7 @@ CONF_ON_TURN_OFF = 'on_turn_off'
 CONF_ON_TURN_ON = 'on_turn_on'
 CONF_ON_VALUE = 'on_value'
 CONF_ON_VALUE_RANGE = 'on_value_range'
+CONF_ONE = 'one'
 CONF_OPEN_ACTION = 'open_action'
 CONF_OPEN_DURATION = 'open_duration'
 CONF_OPEN_ENDSTOP = 'open_endstop'
@@ -283,11 +299,11 @@ CONF_OSCILLATION_OUTPUT = 'oscillation_output'
 CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic'
 CONF_OTA = 'ota'
 CONF_OUTPUT = 'output'
-CONF_OUTPUTS = 'outputs'
 CONF_OUTPUT_ID = 'output_id'
+CONF_OUTPUTS = 'outputs'
 CONF_OVERSAMPLING = 'oversampling'
-CONF_PAGES = 'pages'
 CONF_PAGE_ID = 'page_id'
+CONF_PAGES = 'pages'
 CONF_PANASONIC = 'panasonic'
 CONF_PASSWORD = 'password'
 CONF_PAYLOAD = 'payload'
@@ -295,19 +311,20 @@ CONF_PAYLOAD_AVAILABLE = 'payload_available'
 CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
 CONF_PHASE_BALANCER = 'phase_balancer'
 CONF_PIN = 'pin'
-CONF_PINS = 'pins'
 CONF_PIN_A = 'pin_a'
 CONF_PIN_B = 'pin_b'
 CONF_PIN_C = 'pin_c'
 CONF_PIN_D = 'pin_d'
+CONF_PINS = 'pins'
 CONF_PLATFORM = 'platform'
 CONF_PLATFORMIO_OPTIONS = 'platformio_options'
-CONF_PM_10_0 = 'pm_10_0'
 CONF_PM_1_0 = 'pm_1_0'
+CONF_PM_10_0 = 'pm_10_0'
 CONF_PM_2_5 = 'pm_2_5'
 CONF_PORT = 'port'
 CONF_POSITION = 'position'
 CONF_POWER = 'power'
+CONF_POWER_FACTOR = 'power_factor'
 CONF_POWER_ON_VALUE = 'power_on_value'
 CONF_POWER_SAVE_MODE = 'power_save_mode'
 CONF_POWER_SUPPLY = 'power_supply'
@@ -336,8 +353,8 @@ CONF_RESTORE_MODE = 'restore_mode'
 CONF_RESTORE_STATE = 'restore_state'
 CONF_RESTORE_VALUE = 'restore_value'
 CONF_RETAIN = 'retain'
-CONF_RGBW = 'rgbw'
 CONF_RGB_ORDER = 'rgb_order'
+CONF_RGBW = 'rgbw'
 CONF_RISING_EDGE = 'rising_edge'
 CONF_ROTATION = 'rotation'
 CONF_RS_PIN = 'rs_pin'
@@ -349,7 +366,6 @@ CONF_RX_PIN = 'rx_pin'
 CONF_SAFE_MODE = 'safe_mode'
 CONF_SAMSUNG = 'samsung'
 CONF_SCAN = 'scan'
-CONF_SCAN_INTERVAL = 'scan_interval'
 CONF_SCL = 'scl'
 CONF_SCL_PIN = 'scl_pin'
 CONF_SDA = 'sda'
@@ -361,14 +377,14 @@ CONF_SEL_PIN = 'sel_pin'
 CONF_SEND_EVERY = 'send_every'
 CONF_SEND_FIRST_AT = 'send_first_at'
 CONF_SENSOR = 'sensor'
-CONF_SENSORS = 'sensors'
 CONF_SENSOR_ID = 'sensor_id'
+CONF_SENSORS = 'sensors'
+CONF_SEQUENCE = 'sequence'
 CONF_SERVERS = 'servers'
 CONF_SERVICE = 'service'
 CONF_SERVICES = 'services'
 CONF_SETUP_MODE = 'setup_mode'
 CONF_SETUP_PRIORITY = 'setup_priority'
-CONF_SEQUENCE = 'sequence'
 CONF_SHUNT_RESISTANCE = 'shunt_resistance'
 CONF_SHUNT_VOLTAGE = 'shunt_voltage'
 CONF_SHUTDOWN_MESSAGE = 'shutdown_message'
@@ -381,6 +397,7 @@ CONF_SPEED = 'speed'
 CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic'
 CONF_SPEED_STATE_TOPIC = 'speed_state_topic'
 CONF_SPI_ID = 'spi_id'
+CONF_SPIKE_REJECTION = 'spike_rejection'
 CONF_SSID = 'ssid'
 CONF_SSL_FINGERPRINTS = 'ssl_fingerprints'
 CONF_STATE = 'state'
@@ -391,6 +408,8 @@ CONF_STEP_PIN = 'step_pin'
 CONF_STOP = 'stop'
 CONF_STOP_ACTION = 'stop_action'
 CONF_SUBNET = 'subnet'
+CONF_SUPPORTS_COOL = 'supports_cool'
+CONF_SUPPORTS_HEAT = 'supports_heat'
 CONF_SWITCHES = 'switches'
 CONF_SYNC = 'sync'
 CONF_TAG = 'tag'
@@ -405,11 +424,13 @@ CONF_THEN = 'then'
 CONF_THRESHOLD = 'threshold'
 CONF_THROTTLE = 'throttle'
 CONF_TILT = 'tilt'
+CONF_TILT_ACTION = 'tilt_action'
+CONF_TILT_LAMBDA = 'tilt_lambda'
 CONF_TIME = 'time'
+CONF_TIME_ID = 'time_id'
 CONF_TIMEOUT = 'timeout'
 CONF_TIMES = 'times'
 CONF_TIMEZONE = 'timezone'
-CONF_TIME_ID = 'time_id'
 CONF_TIMING = 'timing'
 CONF_TO = 'to'
 CONF_TOLERANCE = 'tolerance'
@@ -430,8 +451,8 @@ CONF_UNIQUE = 'unique'
 CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement'
 CONF_UPDATE_INTERVAL = 'update_interval'
 CONF_UPDATE_ON_BOOT = 'update_on_boot'
-CONF_USERNAME = 'username'
 CONF_USE_ADDRESS = 'use_address'
+CONF_USERNAME = 'username'
 CONF_UUID = 'uuid'
 CONF_VALUE = 'value'
 CONF_VARIABLES = 'variables'
@@ -445,11 +466,14 @@ CONF_WAIT_UNTIL = 'wait_until'
 CONF_WAKEUP_PIN = 'wakeup_pin'
 CONF_WARM_WHITE = 'warm_white'
 CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature'
+CONF_WATCHDOG_THRESHOLD = 'watchdog_threshold'
 CONF_WHILE = 'while'
 CONF_WHITE = 'white'
 CONF_WIDTH = 'width'
 CONF_WIFI = 'wifi'
 CONF_WILL_MESSAGE = 'will_message'
+CONF_WIND_DIRECTION_DEGREES = 'wind_direction_degrees'
+CONF_WIND_SPEED = 'wind_speed'
 CONF_WINDOW_SIZE = 'window_size'
 CONF_ZERO = 'zero'
 
@@ -457,8 +481,9 @@ ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical'
 ICON_BATTERY = 'mdi:battery'
 ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download'
 ICON_BRIGHTNESS_5 = 'mdi:brightness-5'
-ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon'
 ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline'
+ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon'
+ICON_CURRENT_AC = 'mdi:current-ac'
 ICON_EMPTY = ''
 ICON_FLASH = 'mdi:flash'
 ICON_FLOWER = 'mdi:flower'
@@ -476,23 +501,29 @@ ICON_RESTART = 'mdi:restart'
 ICON_ROTATE_RIGHT = 'mdi:rotate-right'
 ICON_SCALE = 'mdi:scale'
 ICON_SCREEN_ROTATION = 'mdi:screen-rotation'
-ICON_SIGNAL = 'mdi:signal'
-ICON_WEATHER_SUNSET = 'mdi:weather-sunset'
-ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down'
-ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up'
+ICON_SIGN_DIRECTION = 'mdi:sign-direction'
+ICON_SIGNAL = 'mdi: signal-distance-variant'
+ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal'
 ICON_THERMOMETER = 'mdi:thermometer'
 ICON_TIMER = 'mdi:timer'
 ICON_WATER_PERCENT = 'mdi:water-percent'
+ICON_WEATHER_SUNSET = 'mdi:weather-sunset'
+ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down'
+ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up'
+ICON_WEATHER_WINDY = 'mdi:weather-windy'
 ICON_WIFI = 'mdi:wifi'
 
 UNIT_AMPERE = 'A'
 UNIT_CELSIUS = u'°C'
 UNIT_DECIBEL = 'dB'
-UNIT_DEGREES = u'°'
 UNIT_DEGREE_PER_SECOND = u'°/s'
+UNIT_DEGREES = u'°'
 UNIT_EMPTY = ''
 UNIT_HECTOPASCAL = 'hPa'
+UNIT_HZ = 'hz'
 UNIT_KELVIN = 'K'
+UNIT_KILOMETER = 'km'
+UNIT_KILOMETER_PER_HOUR = 'km/h'
 UNIT_LUX = 'lx'
 UNIT_METER = 'm'
 UNIT_METER_PER_SECOND_SQUARED = u'm/s²'
@@ -500,8 +531,8 @@ UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³'
 UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm'
 UNIT_MICROTESLA = u'µT'
 UNIT_OHM = u'Ω'
-UNIT_PARTS_PER_MILLION = 'ppm'
 UNIT_PARTS_PER_BILLION = 'ppb'
+UNIT_PARTS_PER_MILLION = 'ppm'
 UNIT_PERCENT = '%'
 UNIT_PULSES_PER_MINUTE = 'pulses/min'
 UNIT_SECOND = 's'
diff --git a/esphome/core.py b/esphome/core.py
index 7aaf6b2c70..82ce196cab 100644
--- a/esphome/core.py
+++ b/esphome/core.py
@@ -10,8 +10,8 @@ import re
 # pylint: disable=unused-import, wrong-import-order
 from typing import Any, Dict, List  # noqa
 
-from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI, \
-    SOURCE_FILE_EXTENSIONS
+from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \
+    CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI
 from esphome.helpers import ensure_unique_string, is_hassio
 from esphome.py_compat import IS_PY2, integer_types, text_type, string_types
 from esphome.util import OrderedDict
@@ -495,7 +495,7 @@ class EsphomeCore(object):
         # A list of statements to insert in the global block (includes and global variables)
         self.global_statements = []  # type: List[Statement]
         # A set of platformio libraries to add to the project
-        self.libraries = set()  # type: Set[Library]
+        self.libraries = []  # type: List[Library]
         # A set of build flags to set in the platformio project
         self.build_flags = set()  # type: Set[str]
         # A set of defines to set for the compile process in esphome/core/defines.h
@@ -507,6 +507,8 @@ class EsphomeCore(object):
         self.loaded_integrations = set()
         # A set of component IDs to track what Component subclasses are declared
         self.component_ids = set()
+        # Whether ESPHome was started in verbose mode
+        self.verbose = False
 
     def reset(self):
         self.dashboard = False
@@ -522,7 +524,7 @@ class EsphomeCore(object):
         self.variables = {}
         self.main_statements = []
         self.global_statements = []
-        self.libraries = set()
+        self.libraries = []
         self.build_flags = set()
         self.defines = set()
         self.active_coroutines = {}
@@ -539,6 +541,13 @@ class EsphomeCore(object):
 
         return None
 
+    @property
+    def comment(self):  # type: () -> str
+        if CONF_COMMENT in self.config[CONF_ESPHOME]:
+            return self.config[CONF_ESPHOME][CONF_COMMENT]
+
+        return None
+
     def _add_active_coroutine(self, instance_id, obj):
         self.active_coroutines[instance_id] = obj
 
@@ -666,8 +675,25 @@ class EsphomeCore(object):
         if not isinstance(library, Library):
             raise ValueError(u"Library {} must be instance of Library, not {}"
                              u"".format(library, type(library)))
-        self.libraries.add(library)
         _LOGGER.debug("Adding library: %s", library)
+        for other in self.libraries[:]:
+            if other.name != library.name:
+                continue
+            if library.version is None:
+                # Other requirement is more specific
+                break
+            if other.version is None:
+                # Found more specific version requirement
+                self.libraries.remove(other)
+                continue
+            if other.version == library.version:
+                break
+
+            raise ValueError(u"Version pinning failed! Libraries {} and {} "
+                             u"requested with conflicting versions!"
+                             u"".format(library, other))
+        else:
+            self.libraries.append(library)
         return library
 
     def add_build_flag(self, build_flag):
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index f865643f57..2600ace218 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -33,10 +33,9 @@ void Application::setup() {
 
   for (uint32_t i = 0; i < this->components_.size(); i++) {
     Component *component = this->components_[i];
-    if (component->is_failed())
-      continue;
 
-    component->call_setup();
+    component->call();
+    this->scheduler.process_to_add();
     if (component->can_proceed())
       continue;
 
@@ -45,10 +44,9 @@ void Application::setup() {
 
     do {
       uint32_t new_app_state = STATUS_LED_WARNING;
+      this->scheduler.call();
       for (uint32_t j = 0; j <= i; j++) {
-        if (!this->components_[j]->is_failed()) {
-          this->components_[j]->call_loop();
-        }
+        this->components_[j]->call();
         new_app_state |= this->components_[j]->get_component_state();
         this->app_state_ |= new_app_state;
       }
@@ -63,20 +61,20 @@ void Application::setup() {
 void Application::loop() {
   uint32_t new_app_state = 0;
   const uint32_t start = millis();
+
+  this->scheduler.call();
   for (Component *component : this->components_) {
-    if (!component->is_failed()) {
-      component->call_loop();
-    }
+    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 (%.1f s).", (end - start) / 1e3f);
+    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().");
-    ESP_LOGV(TAG, "This will become a warning soon.");
   }
 
   const uint32_t now = millis();
@@ -87,13 +85,19 @@ void Application::loop() {
     uint32_t delay_time = this->loop_interval_;
     if (now - this->last_loop_ < this->loop_interval_)
       delay_time = this->loop_interval_ - (now - this->last_loop_);
+
+    uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time);
+    // next_schedule is max 0.5*delay_time
+    // otherwise interval=0 schedules result in constant looping with almost no sleep
+    next_schedule = std::max(next_schedule, delay_time / 2);
+    delay_time = std::min(next_schedule, delay_time);
     delay(delay_time);
   }
   this->last_loop_ = now;
 
   if (this->dump_config_at_ >= 0 && this->dump_config_at_ < this->components_.size()) {
     if (this->dump_config_at_ == 0) {
-      ESP_LOGI(TAG, "esphome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
+      ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
     }
 
     this->components_[this->dump_config_at_]->dump_config();
@@ -114,7 +118,7 @@ void ICACHE_RAM_ATTR HOT Application::feed_wdt() {
     LAST_FEED = now;
 #ifdef USE_STATUS_LED
     if (status_led::global_status_led != nullptr) {
-      status_led::global_status_led->call_loop();
+      status_led::global_status_led->call();
     }
 #endif
   }
diff --git a/esphome/core/application.h b/esphome/core/application.h
index c4cc1f27a8..2014b082e9 100644
--- a/esphome/core/application.h
+++ b/esphome/core/application.h
@@ -6,6 +6,7 @@
 #include "esphome/core/preferences.h"
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
+#include "esphome/core/scheduler.h"
 
 #ifdef USE_BINARY_SENSOR
 #include "esphome/components/binary_sensor/binary_sensor.h"
@@ -39,7 +40,7 @@ class Application {
   void pre_setup(const std::string &name, const char *compilation_time) {
     this->name_ = name;
     this->compilation_time_ = compilation_time;
-    global_preferences.begin(this->name_);
+    global_preferences.begin();
   }
 
 #ifdef USE_BINARY_SENSOR
@@ -118,8 +119,12 @@ class Application {
   void safe_reboot();
 
   void run_safe_shutdown_hooks() {
-    for (auto *comp : this->components_)
+    for (auto *comp : this->components_) {
       comp->on_safe_shutdown();
+    }
+    for (auto *comp : this->components_) {
+      comp->on_shutdown();
+    }
   }
 
   uint32_t get_app_state() const { return this->app_state_; }
@@ -197,6 +202,8 @@ class Application {
   }
 #endif
 
+  Scheduler scheduler;
+
  protected:
   friend Component;
 
diff --git a/esphome/core/automation.h b/esphome/core/automation.h
index ceed28e5b8..cbe96a749e 100644
--- a/esphome/core/automation.h
+++ b/esphome/core/automation.h
@@ -17,6 +17,15 @@ namespace esphome {
 
 #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
 
+#define TEMPLATABLE_STRING_VALUE_(name) \
+ protected: \
+  TemplatableStringValue<Ts...> name##_{}; \
+\
+ public: \
+  template<typename V> void set_##name(V name) { this->name##_ = name; }
+
+#define TEMPLATABLE_STRING_VALUE(name) TEMPLATABLE_STRING_VALUE_(name)
+
 /** Base class for all automation conditions.
  *
  * @tparam Ts The template parameters to pass when executing.
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index fbd7439d70..0547fcbdd5 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -1,5 +1,3 @@
-#include <algorithm>
-
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/esphal.h"
@@ -45,109 +43,50 @@ void Component::setup() {}
 void Component::loop() {}
 
 void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&f) {  // NOLINT
-  const uint32_t now = millis();
-  // only put offset in lower half
-  uint32_t offset = 0;
-  if (interval != 0)
-    offset = (random_uint32() % interval) / 2;
-  ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset);
-
-  if (!name.empty()) {
-    this->cancel_interval(name);
-  }
-  struct TimeFunction function = {
-      .name = name,
-      .type = TimeFunction::INTERVAL,
-      .interval = interval,
-      .last_execution = now - interval - offset,
-      .f = std::move(f),
-      .remove = false,
-  };
-  this->time_functions_.push_back(function);
+  App.scheduler.set_interval(this, name, interval, std::move(f));
 }
 
 bool Component::cancel_interval(const std::string &name) {  // NOLINT
-  return this->cancel_time_function_(name, TimeFunction::INTERVAL);
+  return App.scheduler.cancel_interval(this, name);
 }
 
 void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) {  // NOLINT
-  const uint32_t now = millis();
-  ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout);
-
-  if (!name.empty()) {
-    this->cancel_timeout(name);
-  }
-  struct TimeFunction function = {
-      .name = name,
-      .type = TimeFunction::TIMEOUT,
-      .interval = timeout,
-      .last_execution = now,
-      .f = std::move(f),
-      .remove = false,
-  };
-  this->time_functions_.push_back(function);
+  return App.scheduler.set_timeout(this, name, timeout, std::move(f));
 }
 
 bool Component::cancel_timeout(const std::string &name) {  // NOLINT
-  return this->cancel_time_function_(name, TimeFunction::TIMEOUT);
+  return App.scheduler.cancel_timeout(this, name);
 }
 
-void Component::call_loop() {
-  this->loop_internal_();
-  this->loop();
-}
+void Component::call_loop() { this->loop(); }
 
-bool Component::cancel_time_function_(const std::string &name, TimeFunction::Type type) {
-  // NOLINTNEXTLINE
-  for (auto iter = this->time_functions_.begin(); iter != this->time_functions_.end(); iter++) {
-    if (!iter->remove && iter->name == name && iter->type == type) {
-      ESP_LOGVV(TAG, "Removing old time function %s.", iter->name.c_str());
-      iter->remove = true;
-      return true;
-    }
-  }
-  return false;
-}
-void Component::call_setup() {
-  this->setup_internal_();
-  this->setup();
-}
+void Component::call_setup() { this->setup(); }
 uint32_t Component::get_component_state() const { return this->component_state_; }
-void Component::loop_internal_() {
-  this->component_state_ &= ~COMPONENT_STATE_MASK;
-  this->component_state_ |= COMPONENT_STATE_LOOP;
-
-  for (unsigned int i = 0; i < this->time_functions_.size(); i++) {  // NOLINT
-    const uint32_t now = millis();
-    TimeFunction *tf = &this->time_functions_[i];
-    if (tf->should_run(now)) {
-#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
-      const char *type =
-          tf->type == TimeFunction::INTERVAL ? "interval" : (tf->type == TimeFunction::TIMEOUT ? "timeout" : "defer");
-      ESP_LOGVV(TAG, "Running %s '%s':%u with interval=%u last_execution=%u (now=%u)", type, tf->name.c_str(), i,
-                tf->interval, tf->last_execution, now);
-#endif
-
-      tf->f();
-      // The vector might have reallocated due to new items
-      tf = &this->time_functions_[i];
-
-      if (tf->type == TimeFunction::INTERVAL && tf->interval != 0) {
-        const uint32_t amount = (now - tf->last_execution) / tf->interval;
-        tf->last_execution += (amount * tf->interval);
-      } else if (tf->type == TimeFunction::DEFER || tf->type == TimeFunction::TIMEOUT) {
-        tf->remove = true;
-      }
-    }
+void Component::call() {
+  uint32_t state = this->component_state_ & COMPONENT_STATE_MASK;
+  switch (state) {
+    case COMPONENT_STATE_CONSTRUCTION:
+      // State Construction: Call setup and set state to setup
+      this->component_state_ &= ~COMPONENT_STATE_MASK;
+      this->component_state_ |= COMPONENT_STATE_SETUP;
+      this->call_setup();
+      break;
+    case COMPONENT_STATE_SETUP:
+      // State setup: Call first loop and set state to loop
+      this->component_state_ &= ~COMPONENT_STATE_MASK;
+      this->component_state_ |= COMPONENT_STATE_LOOP;
+      this->call_loop();
+      break;
+    case COMPONENT_STATE_LOOP:
+      // State loop: Call loop
+      this->call_loop();
+      break;
+    case COMPONENT_STATE_FAILED:
+      // State failed: Do nothing
+      break;
+    default:
+      break;
   }
-
-  this->time_functions_.erase(std::remove_if(this->time_functions_.begin(), this->time_functions_.end(),
-                                             [](const TimeFunction &tf) -> bool { return tf.remove; }),
-                              this->time_functions_.end());
-}
-void Component::setup_internal_() {
-  this->component_state_ &= ~COMPONENT_STATE_MASK;
-  this->component_state_ |= COMPONENT_STATE_SETUP;
 }
 void Component::mark_failed() {
   ESP_LOGE(TAG, "Component was marked as failed.");
@@ -155,29 +94,20 @@ void Component::mark_failed() {
   this->component_state_ |= COMPONENT_STATE_FAILED;
   this->status_set_error();
 }
-void Component::defer(std::function<void()> &&f) { this->defer("", std::move(f)); }  // NOLINT
-bool Component::cancel_defer(const std::string &name) {                              // NOLINT
-  return this->cancel_time_function_(name, TimeFunction::DEFER);
+void Component::defer(std::function<void()> &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, "", 0, std::move(f));
+}
+bool Component::cancel_defer(const std::string &name) {  // NOLINT
+  return App.scheduler.cancel_timeout(this, name);
 }
 void Component::defer(const std::string &name, std::function<void()> &&f) {  // NOLINT
-  if (!name.empty()) {
-    this->cancel_defer(name);
-  }
-  struct TimeFunction function = {
-      .name = name,
-      .type = TimeFunction::DEFER,
-      .interval = 0,
-      .last_execution = 0,
-      .f = std::move(f),
-      .remove = false,
-  };
-  this->time_functions_.push_back(function);
+  App.scheduler.set_timeout(this, name, 0, std::move(f));
 }
 void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) {  // NOLINT
-  this->set_timeout("", timeout, std::move(f));
+  App.scheduler.set_timeout(this, "", timeout, std::move(f));
 }
 void Component::set_interval(uint32_t interval, std::function<void()> &&f) {  // NOLINT
-  this->set_interval("", interval, std::move(f));
+  App.scheduler.set_interval(this, "", interval, std::move(f));
 }
 bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
 bool Component::can_proceed() { return true; }
@@ -203,16 +133,15 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
 }
 void Component::dump_config() {}
 float Component::get_actual_setup_priority() const {
-  return this->setup_priority_override_.value_or(this->get_setup_priority());
+  if (isnan(this->setup_priority_override_))
+    return this->get_setup_priority();
+  return this->setup_priority_override_;
 }
 void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; }
 
 PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {}
 
 void PollingComponent::call_setup() {
-  // Call component internal setup.
-  this->setup_internal_();
-
   // Let the polling component subclass setup their HW.
   this->setup();
 
@@ -240,12 +169,4 @@ void Nameable::calc_object_id_() {
 }
 uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; }
 
-bool Component::TimeFunction::should_run(uint32_t now) const {
-  if (this->remove)
-    return false;
-  if (this->type == DEFER)
-    return true;
-  return this->interval != 4294967295UL && now - this->last_execution > this->interval;
-}
-
 }  // namespace esphome
diff --git a/esphome/core/component.h b/esphome/core/component.h
index 60f306ede4..c8e05cc252 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -2,7 +2,7 @@
 
 #include <string>
 #include <functional>
-#include <vector>
+#include "Arduino.h"
 
 #include "esphome/core/optional.h"
 
@@ -91,18 +91,7 @@ class Component {
    */
   virtual float get_loop_priority() const;
 
-  /** Public loop() functions. These will be called by the Application instance.
-   *
-   * Note: This should normally not be overriden, unless you know what you're doing.
-   * They're basically to make creating custom components easier. For example the
-   * SensorComponent can override these methods to not have the user call some super
-   * methods within their custom sensors. These methods should ALWAYS call the loop_internal()
-   * and setup_internal() methods.
-   *
-   * Basically, it handles stuff like interval/timeout functions and eventually calls loop().
-   */
-  virtual void call_loop();
-  virtual void call_setup();
+  void call();
 
   virtual void on_shutdown() {}
   virtual void on_safe_shutdown() {}
@@ -138,6 +127,8 @@ class Component {
   void status_momentary_error(const std::string &name, uint32_t length = 5000);
 
  protected:
+  virtual void call_loop();
+  virtual void call_setup();
   /** Set an interval function with a unique name. Empty name means no cancelling possible.
    *
    * This will call f every interval ms. Can be cancelled via CancelInterval().
@@ -204,34 +195,8 @@ class Component {
   /// Cancel a defer callback using the specified name, name must not be empty.
   bool cancel_defer(const std::string &name);  // NOLINT
 
-  void loop_internal_();
-  void setup_internal_();
-
-  /// Internal struct for storing timeout/interval functions.
-  struct TimeFunction {
-    std::string name;                             ///< The name/id of this TimeFunction.
-    enum Type { TIMEOUT, INTERVAL, DEFER } type;  ///< The type of this TimeFunction. Either TIMEOUT, INTERVAL or DEFER.
-    uint32_t interval;                            ///< The interval/timeout of this function.
-    /// The last execution for interval functions and the time, SetInterval was called, for timeout functions.
-    uint32_t last_execution;
-    std::function<void()> f;  ///< The function (or callback) itself.
-    bool remove;
-
-    bool should_run(uint32_t now) const;
-  };
-
-  /// Cancel an only time function. If name is empty, won't do anything.
-  bool cancel_time_function_(const std::string &name, TimeFunction::Type type);
-
-  /** Storage for interval/timeout functions.
-   *
-   * Intentionally a vector despite its map-like nature, because of the
-   * memory overhead.
-   */
-  std::vector<TimeFunction> time_functions_;
-
   uint32_t component_state_{0x0000};  ///< State of this component.
-  optional<float> setup_priority_override_;
+  float setup_priority_override_{NAN};
 };
 
 /** This class simplifies creating components that periodically check a state.
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index c52a4390dc..fe10a42baa 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -22,3 +22,4 @@
 #endif
 #define USE_TIME
 #define USE_DEEP_SLEEP
+#define USE_CAPTIVE_PORTAL
diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp
index f0749894c0..13d54e726d 100644
--- a/esphome/core/esphal.cpp
+++ b/esphome/core/esphal.cpp
@@ -148,7 +148,7 @@ void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) {
   }
 #endif
 }
-void ISRInternalGPIOPin::digital_write(bool value) {
+void ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_write(bool value) {
 #ifdef ARDUINO_ARCH_ESP8266
   if (this->pin_ != 16) {
     if (value != this->inverted_) {
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index c65ca919ba..6d6aa80b66 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -314,4 +314,20 @@ std::array<uint8_t, 2> decode_uint16(uint16_t value) {
   return {msb, lsb};
 }
 
+std::string hexencode(const uint8_t *data, uint32_t len) {
+  char buf[20];
+  std::string res;
+  for (size_t i = 0; i < len; i++) {
+    if (i + 1 != len) {
+      sprintf(buf, "%02X.", data[i]);
+    } else {
+      sprintf(buf, "%02X ", data[i]);
+    }
+    res += buf;
+  }
+  sprintf(buf, "(%u)", len);
+  res += buf;
+  return res;
+}
+
 }  // namespace esphome
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index d21cb85b7d..88f0d587e5 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -4,6 +4,7 @@
 #include <functional>
 #include <vector>
 #include <memory>
+#include <type_traits>
 
 #include "esphome/core/optional.h"
 #include "esphome/core/esphal.h"
@@ -155,11 +156,19 @@ enum ParseOnOffState {
 
 ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
 
+// Encode raw data to a human-readable string (for debugging)
+std::string hexencode(const uint8_t *data, uint32_t len);
+
 // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
 template<int...> struct seq {};                                       // NOLINT
 template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {};  // NOLINT
 template<int... S> struct gens<0, S...> { using type = seq<S...>; };  // NOLINT
 
+template<bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type;
+
+template<typename T, enable_if_t<!std::is_pointer<T>::value, int> = 0> T id(T value) { return value; }
+template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T *value) { return *value; }
+
 template<typename... X> class CallbackManager;
 
 /** Simple helper class to allow having multiple subscribers to a signal.
@@ -192,8 +201,6 @@ struct is_callable  // NOLINT
   static constexpr auto value = decltype(test<T>(nullptr))::value;  // NOLINT
 };
 
-template<bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type;
-
 template<typename T, typename... X> class TemplatableValue {
  public:
   TemplatableValue() : type_(EMPTY) {}
@@ -239,6 +246,18 @@ template<typename T, typename... X> class TemplatableValue {
   std::function<T(X...)> f_;
 };
 
+template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
+ public:
+  TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
+
+  template<typename F, enable_if_t<!is_callable<F, X...>::value, int> = 0>
+  TemplatableStringValue(F value) : TemplatableValue<std::string, X...>(value) {}
+
+  template<typename F, enable_if_t<is_callable<F, X...>::value, int> = 0>
+  TemplatableStringValue(F f)
+      : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
+};
+
 void delay_microseconds_accurate(uint32_t usec);
 
 template<typename T> class Deduplicator {
diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp
index 8adaebe5b5..15d49c0038 100644
--- a/esphome/core/log.cpp
+++ b/esphome/core/log.cpp
@@ -1,6 +1,6 @@
-#include "esphome/core/log.h"
-#include "esphome/core/defines.h"
-#include "esphome/core/helpers.h"
+#include "log.h"
+#include "defines.h"
+#include "helpers.h"
 
 #ifdef USE_LOGGER
 #include "esphome/components/logger/logger.h"
@@ -8,60 +8,65 @@
 
 namespace esphome {
 
-int HOT esp_log_printf_(int level, const char *tag, const char *format, ...) {  // NOLINT
+void HOT esp_log_printf_(int level, const char *tag, int line, const char *format, ...) {  // NOLINT
   va_list arg;
   va_start(arg, format);
-  int ret = esp_log_vprintf_(level, tag, format, arg);
+  esp_log_vprintf_(level, tag, line, format, arg);
   va_end(arg);
-  return ret;
 }
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-int HOT esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...) {
+void HOT esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...) {
   va_list arg;
   va_start(arg, format);
-  int ret = esp_log_vprintf_(level, tag, format, arg);
+  esp_log_vprintf_(level, tag, line, format, arg);
   va_end(arg);
-  return ret;
-  return 0;
 }
 #endif
 
-int HOT esp_log_vprintf_(int level, const char *tag, const char *format, va_list args) {  // NOLINT
+void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT
 #ifdef USE_LOGGER
   auto *log = logger::global_logger;
   if (log == nullptr)
-    return 0;
+    return;
 
-  return log->log_vprintf_(level, tag, format, args);
-#else
-  return 0;
+  log->log_vprintf_(level, tag, line, format, args);
 #endif
 }
 
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-int HOT esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) {  // NOLINT
+void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
+                          va_list args) {  // NOLINT
 #ifdef USE_LOGGER
   auto *log = logger::global_logger;
   if (log == nullptr)
-    return 0;
+    return;
 
-  return log->log_vprintf_(level, tag, format, args);
-#else
-  return 0;
+  log->log_vprintf_(level, tag, line, format, args);
 #endif
 }
 #endif
 
+#ifdef ARDUINO_ARCH_ESP32
 int HOT esp_idf_log_vprintf_(const char *format, va_list args) {  // NOLINT
 #ifdef USE_LOGGER
   auto *log = logger::global_logger;
   if (log == nullptr)
     return 0;
 
-  return log->log_vprintf_(log->get_global_log_level(), "", format, args);
-#else
-  return 0;
+  size_t len = strlen(format);
+  if (format[len - 1] == '\n') {
+    // Remove trailing newline from format
+    // Use locally stored
+    static std::string FORMAT_COPY;
+    FORMAT_COPY.clear();
+    FORMAT_COPY.insert(0, format, len - 1);
+    format = FORMAT_COPY.c_str();
+  }
+
+  log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args);
 #endif
+  return 0;
 }
+#endif
 
 }  // namespace esphome
diff --git a/esphome/core/log.h b/esphome/core/log.h
index 4e4d178b96..361fbe1182 100644
--- a/esphome/core/log.h
+++ b/esphome/core/log.h
@@ -20,9 +20,10 @@ namespace esphome {
 #define ESPHOME_LOG_LEVEL_ERROR 1
 #define ESPHOME_LOG_LEVEL_WARN 2
 #define ESPHOME_LOG_LEVEL_INFO 3
-#define ESPHOME_LOG_LEVEL_DEBUG 4
-#define ESPHOME_LOG_LEVEL_VERBOSE 5
-#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 6
+#define ESPHOME_LOG_LEVEL_CONFIG 4
+#define ESPHOME_LOG_LEVEL_DEBUG 5
+#define ESPHOME_LOG_LEVEL_VERBOSE 6
+#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 7
 
 #ifndef ESPHOME_LOG_LEVEL
 #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG
@@ -43,38 +44,30 @@ namespace esphome {
 
 #define ESPHOME_LOG_COLOR(COLOR) "\033[0;" COLOR "m"
 #define ESPHOME_LOG_BOLD(COLOR) "\033[1;" COLOR "m"
-
-#define ESPHOME_LOG_COLOR_E ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED)
-#define ESPHOME_LOG_COLOR_W ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW)
-#define ESPHOME_LOG_COLOR_I ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN)
-#define ESPHOME_LOG_COLOR_C ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA)
-#define ESPHOME_LOG_COLOR_D ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN)
-#define ESPHOME_LOG_COLOR_V ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY)
-#define ESPHOME_LOG_COLOR_VV ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE)
 #define ESPHOME_LOG_RESET_COLOR "\033[0m"
 
-int esp_log_printf_(int level, const char *tag, const char *format, ...)  // NOLINT
-    __attribute__((format(printf, 3, 4)));
+void esp_log_printf_(int level, const char *tag, int line, const char *format, ...)  // NOLINT
+    __attribute__((format(printf, 4, 5)));
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-int esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...);
+void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...);
 #endif
-int esp_log_vprintf_(int level, const char *tag, const char *format, va_list args);  // NOLINT
+void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args);  // NOLINT
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-int esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args);
+void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args);
 #endif
+#ifdef ARDUINO_ARCH_ESP32
 int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
+#endif
 
 #ifdef USE_STORE_LOG_STR_IN_FLASH
-#define ESPHOME_LOG_FORMAT(tag, letter, format) \
-  F(ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR), tag, __LINE__
+#define ESPHOME_LOG_FORMAT(format) F(format)
 #else
-#define ESPHOME_LOG_FORMAT(tag, letter, format) \
-  ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR, tag, __LINE__
+#define ESPHOME_LOG_FORMAT(format) format
 #endif
 
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
 #define esph_log_vv(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, VV, format), ##__VA_ARGS__)
+  esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 
 #define ESPHOME_LOG_HAS_VERY_VERBOSE
 #else
@@ -83,7 +76,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
 
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 #define esph_log_v(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, V, format), ##__VA_ARGS__)
+  esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 
 #define ESPHOME_LOG_HAS_VERBOSE
 #else
@@ -92,22 +85,20 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
 
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 #define esph_log_d(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, D, format), ##__VA_ARGS__)
-
+  esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 #define esph_log_config(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, C, format), ##__VA_ARGS__)
+  esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 
 #define ESPHOME_LOG_HAS_DEBUG
 #define ESPHOME_LOG_HAS_CONFIG
 #else
 #define esph_log_d(tag, format, ...)
-
 #define esph_log_config(tag, format, ...)
 #endif
 
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
 #define esph_log_i(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, ESPHOME_LOG_FORMAT(tag, I, format), ##__VA_ARGS__)
+  esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 
 #define ESPHOME_LOG_HAS_INFO
 #else
@@ -116,7 +107,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
 
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
 #define esph_log_w(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, ESPHOME_LOG_FORMAT(tag, W, format), ##__VA_ARGS__)
+  esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 
 #define ESPHOME_LOG_HAS_WARN
 #else
@@ -125,7 +116,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
 
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
 #define esph_log_e(tag, format, ...) \
-  esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, ESPHOME_LOG_FORMAT(tag, E, format), ##__VA_ARGS__)
+  esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
 
 #define ESPHOME_LOG_HAS_ERROR
 #else
diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp
index 65140bbdc8..2329ed34f5 100644
--- a/esphome/core/preferences.cpp
+++ b/esphome/core/preferences.cpp
@@ -1,20 +1,25 @@
 #include "esphome/core/preferences.h"
 #include "esphome/core/log.h"
 #include "esphome/core/helpers.h"
+#include "esphome/core/application.h"
 
-#ifdef USE_ESP8266_PREFERENCES_FLASH
+#ifdef ARDUINO_ARCH_ESP8266
 extern "C" {
 #include "spi_flash.h"
 }
 #endif
+#ifdef ARDUINO_ARCH_ESP32
+#include "nvs.h"
+#include "nvs_flash.h"
+#endif
 
 namespace esphome {
 
 static const char *TAG = "preferences";
 
-ESPPreferenceObject::ESPPreferenceObject() : rtc_offset_(0), length_words_(0), type_(0), data_(nullptr) {}
-ESPPreferenceObject::ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type)
-    : rtc_offset_(rtc_offset), length_words_(length), type_(type) {
+ESPPreferenceObject::ESPPreferenceObject() : offset_(0), length_words_(0), type_(0), data_(nullptr) {}
+ESPPreferenceObject::ESPPreferenceObject(size_t offset, size_t length, uint32_t type)
+    : offset_(offset), length_words_(length), type_(type) {
   this->data_ = new uint32_t[this->length_words_ + 1];
   for (uint32_t i = 0; i < this->length_words_ + 1; i++)
     this->data_[i] = 0;
@@ -29,7 +34,7 @@ bool ESPPreferenceObject::load_() {
 
   bool valid = this->data_[this->length_words_] == this->calculate_crc_();
 
-  ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_,  // NOLINT
+  ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_,  // NOLINT
             YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_());
   return valid;
 }
@@ -42,17 +47,23 @@ bool ESPPreferenceObject::save_() {
   this->data_[this->length_words_] = this->calculate_crc_();
   if (!this->save_internal_())
     return false;
-  ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_,  // NOLINT
+  ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_,  // NOLINT
             this->data_[0], this->data_[1], this->type_, this->calculate_crc_());
   return true;
 }
 
 #ifdef ARDUINO_ARCH_ESP8266
 
-#define ESP_RTC_USER_MEM_START 0x60001200
+static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
 #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
-#define ESP_RTC_USER_MEM_SIZE_WORDS 128
-#define ESP_RTC_USER_MEM_SIZE_BYTES ESP_RTC_USER_MEM_SIZE_WORDS * 4
+static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
+static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
+
+#ifdef USE_ESP8266_PREFERENCES_FLASH
+static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
+#else
+static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
+#endif
 
 static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
   if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
@@ -62,9 +73,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
   return true;
 }
 
-#ifdef USE_ESP8266_PREFERENCES_FLASH
-static bool esp8266_preferences_modified = false;
-#endif
+static bool esp8266_flash_dirty = false;
 
 static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
   if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
@@ -75,29 +84,24 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
   }
 
   auto *ptr = &ESP_RTC_USER_MEM[index];
-#ifdef USE_ESP8266_PREFERENCES_FLASH
-  if (*ptr != value) {
-    esp8266_preferences_modified = true;
-  }
-#endif
   *ptr = value;
   return true;
 }
 
-#ifdef USE_ESP8266_PREFERENCES_FLASH
 extern "C" uint32_t _SPIFFS_end;
 
-static const uint32_t get_esp8266_flash_sector() { return (uint32_t(&_SPIFFS_end) - 0x40200000) / SPI_FLASH_SEC_SIZE; }
+static const uint32_t get_esp8266_flash_sector() {
+  union {
+    uint32_t *ptr;
+    uint32_t uint;
+  } data{};
+  data.ptr = &_SPIFFS_end;
+  return (data.uint - 0x40200000) / SPI_FLASH_SEC_SIZE;
+}
 static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; }
 
-static void load_esp8266_flash() {
-  ESP_LOGVV(TAG, "Loading preferences from flash...");
-  disable_interrupts();
-  spi_flash_read(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES);
-  enable_interrupts();
-}
-static void save_esp8266_flash() {
-  if (!esp8266_preferences_modified)
+void ESPPreferences::save_esp8266_flash_() {
+  if (!esp8266_flash_dirty)
     return;
 
   ESP_LOGVV(TAG, "Saving preferences to flash...");
@@ -109,31 +113,53 @@ static void save_esp8266_flash() {
     return;
   }
 
-  auto write_res = spi_flash_write(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES);
+  auto write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4);
   enable_interrupts();
   if (write_res != SPI_FLASH_RESULT_OK) {
     ESP_LOGV(TAG, "Write ESP8266 flash failed!");
     return;
   }
 
-  esp8266_preferences_modified = false;
+  esp8266_flash_dirty = false;
 }
-#endif
 
 bool ESPPreferenceObject::save_internal_() {
+  if (this->in_flash_) {
+    for (uint32_t i = 0; i <= this->length_words_; i++) {
+      uint32_t j = this->offset_ + i;
+      if (j >= ESP8266_FLASH_STORAGE_SIZE)
+        return false;
+      uint32_t v = this->data_[i];
+      uint32_t *ptr = &global_preferences.flash_storage_[j];
+      if (*ptr != v)
+        esp8266_flash_dirty = true;
+      *ptr = v;
+    }
+    global_preferences.save_esp8266_flash_();
+    return true;
+  }
+
   for (uint32_t i = 0; i <= this->length_words_; i++) {
-    if (!esp_rtc_user_mem_write(this->rtc_offset_ + i, this->data_[i]))
+    if (!esp_rtc_user_mem_write(this->offset_ + i, this->data_[i]))
       return false;
   }
 
-#ifdef USE_ESP8266_PREFERENCES_FLASH
-  save_esp8266_flash();
-#endif
   return true;
 }
 bool ESPPreferenceObject::load_internal_() {
+  if (this->in_flash_) {
+    for (uint32_t i = 0; i <= this->length_words_; i++) {
+      uint32_t j = this->offset_ + i;
+      if (j >= ESP8266_FLASH_STORAGE_SIZE)
+        return false;
+      this->data_[i] = global_preferences.flash_storage_[j];
+    }
+
+    return true;
+  }
+
   for (uint32_t i = 0; i <= this->length_words_; i++) {
-    if (!esp_rtc_user_mem_read(this->rtc_offset_ + i, &this->data_[i]))
+    if (!esp_rtc_user_mem_read(this->offset_ + i, &this->data_[i]))
       return false;
   }
   return true;
@@ -144,13 +170,26 @@ ESPPreferences::ESPPreferences()
     // which will be reset each time OTA occurs
     : current_offset_(0) {}
 
-void ESPPreferences::begin(const std::string &name) {
-#ifdef USE_ESP8266_PREFERENCES_FLASH
-  load_esp8266_flash();
-#endif
+void ESPPreferences::begin() {
+  this->flash_storage_ = new uint32_t[ESP8266_FLASH_STORAGE_SIZE];
+  ESP_LOGVV(TAG, "Loading preferences from flash...");
+  disable_interrupts();
+  spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4);
+  enable_interrupts();
 }
 
-ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) {
+ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
+  if (in_flash) {
+    uint32_t start = this->current_flash_offset_;
+    uint32_t end = start + length + 1;
+    if (end > ESP8266_FLASH_STORAGE_SIZE)
+      return {};
+    auto pref = ESPPreferenceObject(start, length, type);
+    pref.in_flash_ = true;
+    this->current_flash_offset_ = end;
+    return pref;
+  }
+
   uint32_t start = this->current_offset_;
   uint32_t end = start + length + 1;
   bool in_normal = start < 96;
@@ -165,7 +204,7 @@ ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type
 
   if (end > 128) {
     // Doesn't fit in data, return uninitialized preference obj.
-    return ESPPreferenceObject();
+    return {};
   }
 
   uint32_t rtc_offset;
@@ -185,35 +224,67 @@ bool ESPPreferences::is_prevent_write() { return this->prevent_write_; }
 
 #ifdef ARDUINO_ARCH_ESP32
 bool ESPPreferenceObject::save_internal_() {
+  if (global_preferences.nvs_handle_ == 0)
+    return false;
+
   char key[32];
-  sprintf(key, "%u", this->rtc_offset_);
+  sprintf(key, "%u", this->offset_);
   uint32_t len = (this->length_words_ + 1) * 4;
-  size_t ret = global_preferences.preferences_.putBytes(key, this->data_, len);
-  if (ret != len) {
-    ESP_LOGV(TAG, "putBytes failed!");
+  esp_err_t err = nvs_set_blob(global_preferences.nvs_handle_, key, this->data_, len);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", key, len, esp_err_to_name(err));
+    return false;
+  }
+  err = nvs_commit(global_preferences.nvs_handle_);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_commit('%s', len=%u) failed: %s", key, len, esp_err_to_name(err));
     return false;
   }
   return true;
 }
 bool ESPPreferenceObject::load_internal_() {
+  if (global_preferences.nvs_handle_ == 0)
+    return false;
+
   char key[32];
-  sprintf(key, "%u", this->rtc_offset_);
+  sprintf(key, "%u", this->offset_);
   uint32_t len = (this->length_words_ + 1) * 4;
-  size_t ret = global_preferences.preferences_.getBytes(key, this->data_, len);
-  if (ret != len) {
-    ESP_LOGV(TAG, "getBytes failed!");
+
+  uint32_t actual_len;
+  esp_err_t err = nvs_get_blob(global_preferences.nvs_handle_, key, nullptr, &actual_len);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key, esp_err_to_name(err));
+    return false;
+  }
+  if (actual_len != len) {
+    ESP_LOGVV(TAG, "NVS length does not match. Assuming key changed (%u!=%u)", actual_len, len);
+    return false;
+  }
+  err = nvs_get_blob(global_preferences.nvs_handle_, key, this->data_, &len);
+  if (err) {
+    ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key, esp_err_to_name(err));
     return false;
   }
   return true;
 }
 ESPPreferences::ESPPreferences() : current_offset_(0) {}
-void ESPPreferences::begin(const std::string &name) {
-  const std::string key = truncate_string(name, 15);
-  ESP_LOGV(TAG, "Opening preferences with key '%s'", key.c_str());
-  this->preferences_.begin(key.c_str());
+void ESPPreferences::begin() {
+  auto ns = truncate_string(App.get_name(), 15);
+  esp_err_t err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_);
+  if (err) {
+    ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS...", esp_err_to_name(err));
+    nvs_flash_deinit();
+    nvs_flash_erase();
+    nvs_flash_init();
+
+    err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_);
+    if (err) {
+      this->nvs_handle_ = 0;
+    }
+  }
 }
 
-ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) {
+ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
   auto pref = ESPPreferenceObject(this->current_offset_, length, type);
   this->current_offset_++;
   return pref;
diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h
index 0574c757fb..bfea4c2336 100644
--- a/esphome/core/preferences.h
+++ b/esphome/core/preferences.h
@@ -2,10 +2,6 @@
 
 #include <string>
 
-#ifdef ARDUINO_ARCH_ESP32
-#include <Preferences.h>
-#endif
-
 #include "esphome/core/esphal.h"
 #include "esphome/core/defines.h"
 
@@ -14,7 +10,7 @@ namespace esphome {
 class ESPPreferenceObject {
  public:
   ESPPreferenceObject();
-  ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type);
+  ESPPreferenceObject(size_t offset, size_t length, uint32_t type);
 
   template<typename T> bool save(T *src);
 
@@ -23,6 +19,8 @@ class ESPPreferenceObject {
   bool is_initialized() const;
 
  protected:
+  friend class ESPPreferences;
+
   bool save_();
   bool load_();
   bool save_internal_();
@@ -30,18 +28,33 @@ class ESPPreferenceObject {
 
   uint32_t calculate_crc_() const;
 
-  size_t rtc_offset_;
+  size_t offset_;
   size_t length_words_;
   uint32_t type_;
   uint32_t *data_;
+#ifdef ARDUINO_ARCH_ESP8266
+  bool in_flash_{false};
+#endif
 };
 
+#ifdef ARDUINO_ARCH_ESP8266
+#ifdef USE_ESP8266_PREFERENCES_FLASH
+static bool DEFAULT_IN_FLASH = true;
+#else
+static bool DEFAULT_IN_FLASH = false;
+#endif
+#endif
+
+#ifdef ARDUINO_ARCH_ESP32
+static bool DEFAULT_IN_FLASH = true;
+#endif
+
 class ESPPreferences {
  public:
   ESPPreferences();
-  void begin(const std::string &name);
-  ESPPreferenceObject make_preference(size_t length, uint32_t type);
-  template<typename T> ESPPreferenceObject make_preference(uint32_t type);
+  void begin();
+  ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash = DEFAULT_IN_FLASH);
+  template<typename T> ESPPreferenceObject make_preference(uint32_t type, bool in_flash = DEFAULT_IN_FLASH);
 
 #ifdef ARDUINO_ARCH_ESP8266
   /** On the ESP8266, we can't override the first 128 bytes during OTA uploads
@@ -60,17 +73,20 @@ class ESPPreferences {
 
   uint32_t current_offset_;
 #ifdef ARDUINO_ARCH_ESP32
-  Preferences preferences_;
+  uint32_t nvs_handle_;
 #endif
 #ifdef ARDUINO_ARCH_ESP8266
+  void save_esp8266_flash_();
   bool prevent_write_{false};
+  uint32_t *flash_storage_;
+  uint32_t current_flash_offset_;
 #endif
 };
 
 extern ESPPreferences global_preferences;
 
-template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type) {
-  return this->make_preference((sizeof(T) + 3) / 4, type);
+template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type, bool in_flash) {
+  return this->make_preference((sizeof(T) + 3) / 4, type, in_flash);
 }
 
 template<typename T> bool ESPPreferenceObject::save(T *src) {
diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
new file mode 100644
index 0000000000..cc4331b38e
--- /dev/null
+++ b/esphome/core/scheduler.cpp
@@ -0,0 +1,246 @@
+#include "scheduler.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include <algorithm>
+
+namespace esphome {
+
+static const char *TAG = "scheduler";
+
+static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
+
+// Uncomment to debug scheduler
+// #define ESPHOME_DEBUG_SCHEDULER
+
+void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
+                                std::function<void()> &&func) {
+  const uint32_t now = this->millis_();
+
+  if (!name.empty())
+    this->cancel_timeout(component, name);
+
+  if (timeout == SCHEDULER_DONT_RUN)
+    return;
+
+  ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout);
+
+  auto item = make_unique<SchedulerItem>();
+  item->component = component;
+  item->name = name;
+  item->type = SchedulerItem::TIMEOUT;
+  item->timeout = timeout;
+  item->last_execution = now;
+  item->last_execution_major = this->millis_major_;
+  item->f = std::move(func);
+  item->remove = false;
+  this->push_(std::move(item));
+}
+bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) {
+  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
+}
+void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
+                                 std::function<void()> &&func) {
+  const uint32_t now = this->millis_();
+
+  if (!name.empty())
+    this->cancel_interval(component, name);
+
+  if (interval == SCHEDULER_DONT_RUN)
+    return;
+
+  // only put offset in lower half
+  uint32_t offset = 0;
+  if (interval != 0)
+    offset = (random_uint32() % interval) / 2;
+
+  ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset);
+
+  auto item = make_unique<SchedulerItem>();
+  item->component = component;
+  item->name = name;
+  item->type = SchedulerItem::INTERVAL;
+  item->interval = interval;
+  item->last_execution = now - offset - interval;
+  item->last_execution_major = this->millis_major_;
+  if (item->last_execution > now)
+    item->last_execution_major--;
+  item->f = std::move(func);
+  item->remove = false;
+  this->push_(std::move(item));
+}
+bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
+  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
+}
+optional<uint32_t> HOT Scheduler::next_schedule_in() {
+  if (this->empty_())
+    return {};
+  auto &item = this->items_[0];
+  const uint32_t now = this->millis_();
+  uint32_t next_time = item->last_execution + item->interval;
+  if (next_time < now)
+    return 0;
+  return next_time - now;
+}
+void ICACHE_RAM_ATTR HOT Scheduler::call() {
+  const uint32_t now = this->millis_();
+  this->process_to_add();
+
+#ifdef ESPHOME_DEBUG_SCHEDULER
+  static uint32_t last_print = 0;
+
+  if (now - last_print > 2000) {
+    last_print = now;
+    std::vector<std::unique_ptr<SchedulerItem>> old_items;
+    ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
+    while (!this->empty_()) {
+      auto item = std::move(this->items_[0]);
+      const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
+      ESP_LOGVV(TAG, "  %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(),
+                item->interval, item->last_execution, item->last_execution_major, item->next_execution(),
+                item->next_execution_major());
+
+      this->pop_raw_();
+      old_items.push_back(std::move(item));
+    }
+    ESP_LOGVV(TAG, "\n");
+    this->items_ = std::move(old_items);
+  }
+#endif  // ESPHOME_DEBUG_SCHEDULER
+
+  while (!this->empty_()) {
+    // use scoping to indicate visibility of `item` variable
+    {
+      // Don't copy-by value yet
+      auto &item = this->items_[0];
+      if ((now - item->last_execution) < item->interval)
+        // Not reached timeout yet, done for this call
+        break;
+      uint8_t major = item->next_execution_major();
+      if (this->millis_major_ - major > 1)
+        break;
+
+      // Don't run on failed components
+      if (item->component != nullptr && item->component->is_failed()) {
+        this->pop_raw_();
+        continue;
+      }
+
+#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
+      const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
+      ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(),
+                item->interval, item->last_execution, now);
+#endif
+
+      // Warning: During f(), a lot of stuff can happen, including:
+      //  - timeouts/intervals get added, potentially invalidating vector pointers
+      //  - timeouts/intervals get cancelled
+      item->f();
+    }
+
+    {
+      // new scope, item from before might have been moved in the vector
+      auto item = std::move(this->items_[0]);
+
+      // Only pop after function call, this ensures we were reachable
+      // during the function call and know if we were cancelled.
+      this->pop_raw_();
+
+      if (item->remove) {
+        // We were removed/cancelled in the function call, stop
+        continue;
+      }
+
+      if (item->type == SchedulerItem::INTERVAL) {
+        if (item->interval != 0) {
+          const uint32_t before = item->last_execution;
+          const uint32_t amount = (now - item->last_execution) / item->interval;
+          item->last_execution += amount * item->interval;
+          if (item->last_execution < before)
+            item->last_execution_major++;
+        }
+        this->push_(std::move(item));
+      }
+    }
+  }
+
+  this->process_to_add();
+}
+void HOT Scheduler::process_to_add() {
+  for (auto &it : this->to_add_) {
+    if (it->remove) {
+      continue;
+    }
+
+    this->items_.push_back(std::move(it));
+    std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
+  }
+  this->to_add_.clear();
+}
+void HOT Scheduler::cleanup_() {
+  while (!this->items_.empty()) {
+    auto &item = this->items_[0];
+    if (!item->remove)
+      return;
+
+    this->pop_raw_();
+  }
+}
+void HOT Scheduler::pop_raw_() {
+  std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
+  this->items_.pop_back();
+}
+void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) { this->to_add_.push_back(std::move(item)); }
+bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
+  bool ret = false;
+  for (auto &it : this->items_)
+    if (it->component == component && it->name == name && it->type == type) {
+      it->remove = true;
+      ret = true;
+    }
+  for (auto &it : this->to_add_)
+    if (it->component == component && it->name == name && it->type == type) {
+      it->remove = true;
+      ret = true;
+    }
+
+  return ret;
+}
+uint32_t Scheduler::millis_() {
+  const uint32_t now = millis();
+  if (now < this->last_millis_) {
+    ESP_LOGD(TAG, "Incrementing scheduler major");
+    this->millis_major_++;
+  }
+  return now;
+}
+
+bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
+                                       const std::unique_ptr<SchedulerItem> &b) {
+  // min-heap
+  // return true if *a* will happen after *b*
+  uint32_t a_next_exec = a->next_execution();
+  uint8_t a_next_exec_major = a->next_execution_major();
+  uint32_t b_next_exec = b->next_execution();
+  uint8_t b_next_exec_major = b->next_execution_major();
+
+  if (a_next_exec_major != b_next_exec_major) {
+    // The "major" calculation is quite complicated.
+    // Basically, we need to check if the major value lies in the future or
+    //
+
+    // Here are some cases to think about:
+    // Format: a_major,b_major -> expected result (a-b, b-a)
+    // a=255,b=0 -> false (255, 1)
+    // a=0,b=1 -> false   (255, 1)
+    // a=1,b=0 -> true    (1, 255)
+    // a=0,b=255 -> true  (1, 255)
+
+    uint8_t diff1 = a_next_exec_major - b_next_exec_major;
+    uint8_t diff2 = b_next_exec_major - a_next_exec_major;
+    return diff1 < diff2;
+  }
+
+  return a_next_exec > b_next_exec;
+}
+
+}  // namespace esphome
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
new file mode 100644
index 0000000000..5688058a1e
--- /dev/null
+++ b/esphome/core/scheduler.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include <vector>
+#include <memory>
+
+namespace esphome {
+
+class Component;
+
+class Scheduler {
+ public:
+  void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> &&func);
+  bool cancel_timeout(Component *component, const std::string &name);
+  void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func);
+  bool cancel_interval(Component *component, const std::string &name);
+
+  optional<uint32_t> next_schedule_in();
+
+  void call();
+
+  void process_to_add();
+
+ protected:
+  struct SchedulerItem {
+    Component *component;
+    std::string name;
+    enum Type { TIMEOUT, INTERVAL } type;
+    union {
+      uint32_t interval;
+      uint32_t timeout;
+    };
+    uint32_t last_execution;
+    std::function<void()> f;
+    bool remove;
+    uint8_t last_execution_major;
+
+    inline uint32_t next_execution() { return this->last_execution + this->timeout; }
+    inline uint8_t next_execution_major() {
+      uint32_t next_exec = this->next_execution();
+      uint8_t next_exec_major = this->last_execution_major;
+      if (next_exec < this->last_execution)
+        next_exec_major++;
+      return next_exec_major;
+    }
+
+    static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
+  };
+
+  uint32_t millis_();
+  void cleanup_();
+  void pop_raw_();
+  void push_(std::unique_ptr<SchedulerItem> item);
+  bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
+  bool empty_() {
+    this->cleanup_();
+    return this->items_.empty();
+  }
+
+  std::vector<std::unique_ptr<SchedulerItem>> items_;
+  std::vector<std::unique_ptr<SchedulerItem>> to_add_;
+  uint32_t last_millis_{0};
+  uint8_t millis_major_{0};
+};
+
+}  // namespace esphome
diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp
index 4a2985e516..ea5e347c72 100644
--- a/esphome/core/util.cpp
+++ b/esphome/core/util.cpp
@@ -2,6 +2,7 @@
 #include "esphome/core/defines.h"
 #include "esphome/core/application.h"
 #include "esphome/core/version.h"
+#include "esphome/core/log.h"
 
 #ifdef USE_WIFI
 #include "esphome/components/wifi/wifi_component.h"
@@ -38,84 +39,56 @@ bool network_is_connected() {
   return false;
 }
 
-void network_setup() {
-  bool ready = true;
-#ifdef USE_ETHERNET
-  if (ethernet::global_eth_component != nullptr) {
-    ethernet::global_eth_component->call_setup();
-    ready = false;
-  }
-#endif
-
-#ifdef USE_WIFI
-  if (wifi::global_wifi_component != nullptr) {
-    wifi::global_wifi_component->call_setup();
-    ready = false;
-  }
-#endif
-
-  while (!ready) {
-#ifdef USE_ETHERNET
-    if (ethernet::global_eth_component != nullptr) {
-      ethernet::global_eth_component->call_loop();
-      ready = ready || ethernet::global_eth_component->can_proceed();
-    }
-#endif
-#ifdef USE_WIFI
-    if (wifi::global_wifi_component != nullptr) {
-      wifi::global_wifi_component->call_loop();
-      ready = ready || wifi::global_wifi_component->can_proceed();
-    }
-#endif
-
-    App.feed_wdt();
-  }
-}
-void network_tick() {
-#ifdef USE_ETHERNET
-  if (ethernet::global_eth_component != nullptr)
-    ethernet::global_eth_component->call_loop();
-#endif
-#ifdef USE_WIFI
-  if (wifi::global_wifi_component != nullptr)
-    wifi::global_wifi_component->call_loop();
-#endif
-}
-
-void network_setup_mdns() {
-  MDNS.begin(App.get_name().c_str());
-#ifdef USE_API
-  if (api::global_api_server != nullptr) {
-    MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port());
-    // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version
-    MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION);
-    MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str());
-  } else {
-#endif
-    // Publish "http" service if not using native API.
-    // This is just to have *some* mDNS service so that .local resolution works
-    MDNS.addService("http", "tcp", 80);
-    MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION);
-#ifdef USE_API
-  }
-#endif
-}
-void network_tick_mdns() {
 #ifdef ARDUINO_ARCH_ESP8266
-  MDNS.update();
+bool mdns_setup;
 #endif
-}
 
-std::string network_get_address() {
+#ifdef ARDUINO_ARCH_ESP8266
+void network_setup_mdns(IPAddress address, int interface) {
+  // Latest arduino framework breaks mDNS for AP interface
+  // see https://github.com/esp8266/Arduino/issues/6114
+  if (interface == 1)
+    return;
+  MDNS.begin(App.get_name().c_str(), address);
+  mdns_setup = true;
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+  void network_setup_mdns() {
+    MDNS.begin(App.get_name().c_str());
+#endif
+#ifdef USE_API
+    if (api::global_api_server != nullptr) {
+      MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port());
+      // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version
+      MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION);
+      MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str());
+    } else {
+#endif
+      // Publish "http" service if not using native API.
+      // This is just to have *some* mDNS service so that .local resolution works
+      MDNS.addService("http", "tcp", 80);
+      MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION);
+#ifdef USE_API
+    }
+#endif
+  }
+  void network_tick_mdns() {
+#ifdef ARDUINO_ARCH_ESP8266
+    if (mdns_setup)
+      MDNS.update();
+#endif
+  }
+
+  std::string network_get_address() {
 #ifdef USE_ETHERNET
-  if (ethernet::global_eth_component != nullptr)
-    return ethernet::global_eth_component->get_use_address();
+    if (ethernet::global_eth_component != nullptr)
+      return ethernet::global_eth_component->get_use_address();
 #endif
 #ifdef USE_WIFI
-  if (wifi::global_wifi_component != nullptr)
-    return wifi::global_wifi_component->get_use_address();
+    if (wifi::global_wifi_component != nullptr)
+      return wifi::global_wifi_component->get_use_address();
 #endif
-  return "";
-}
+    return "";
+  }
 
 }  // namespace esphome
diff --git a/esphome/core/util.h b/esphome/core/util.h
index f47eeb8439..0e121ef382 100644
--- a/esphome/core/util.h
+++ b/esphome/core/util.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <string>
+#include "IPAddress.h"
 
 namespace esphome {
 
@@ -10,9 +11,12 @@ bool network_is_connected();
 std::string network_get_address();
 
 /// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode)
-void network_setup();
-void network_tick();
+#ifdef ARDUINO_ARCH_ESP8266
+void network_setup_mdns(IPAddress address, int interface);
+#endif
+#ifdef ARDUINO_ARCH_ESP32
 void network_setup_mdns();
+#endif
 void network_tick_mdns();
 
 }  // namespace esphome
diff --git a/esphome/core_config.py b/esphome/core_config.py
index 051de896b5..f63d2e17e3 100644
--- a/esphome/core_config.py
+++ b/esphome/core_config.py
@@ -7,7 +7,7 @@ import esphome.config_validation as cv
 from esphome import automation, pins
 from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \
     CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \
-    CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \
+    CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \
     CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \
     CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \
     CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266_2_3_0, \
@@ -45,14 +45,14 @@ def validate_board(value):
 validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True)
 
 PLATFORMIO_ESP8266_LUT = {
-    '2.5.2': 'espressif8266@2.2.0',
+    '2.5.2': 'espressif8266@2.2.3',
     '2.5.1': 'espressif8266@2.1.0',
     '2.5.0': 'espressif8266@2.0.1',
     '2.4.2': 'espressif8266@1.8.0',
     '2.4.1': 'espressif8266@1.7.3',
     '2.4.0': 'espressif8266@1.6.0',
     '2.3.0': 'espressif8266@1.5.0',
-    'RECOMMENDED': 'espressif8266@1.8.0',
+    'RECOMMENDED': 'espressif8266@2.2.3',
     'LATEST': 'espressif8266',
     'DEV': ARDUINO_VERSION_ESP8266_DEV,
 }
@@ -60,8 +60,10 @@ PLATFORMIO_ESP8266_LUT = {
 PLATFORMIO_ESP32_LUT = {
     '1.0.0': 'espressif32@1.4.0',
     '1.0.1': 'espressif32@1.6.0',
-    '1.0.2': 'espressif32@1.8.0',
-    'RECOMMENDED': 'espressif32@1.6.0',
+    '1.0.2': 'espressif32@1.9.0',
+    '1.0.3': 'espressif32@1.10.0',
+    '1.0.4': 'espressif32@1.11.0',
+    'RECOMMENDED': 'espressif32@1.11.0',
     'LATEST': 'espressif32',
     'DEV': ARDUINO_VERSION_ESP32_DEV,
 }
@@ -113,6 +115,7 @@ CONFIG_SCHEMA = cv.Schema({
     cv.Required(CONF_NAME): cv.valid_name,
     cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', upper=True),
     cv.Required(CONF_BOARD): validate_board,
+    cv.Optional(CONF_COMMENT): cv.string,
     cv.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version,
     cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string,
     cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema({
@@ -239,7 +242,6 @@ def to_code(config):
 
     # Libraries
     if CORE.is_esp32:
-        cg.add_library('Preferences', None)
         cg.add_library('ESPmDNS', None)
     elif CORE.is_esp8266:
         cg.add_library('ESP8266WiFi', None)
diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py
index c1e4a87179..09b542b3cc 100644
--- a/esphome/cpp_generator.py
+++ b/esphome/cpp_generator.py
@@ -250,7 +250,7 @@ class FloatLiteral(Literal):
     def __str__(self):
         if math.isnan(self.float_):
             return u"NAN"
-        return u"{:f}f".format(self.float_)
+        return u"{}f".format(self.float_)
 
 
 # pylint: disable=bad-continuation
diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index 4df7b5a386..c934626da8 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -26,10 +26,10 @@ import tornado.process
 import tornado.web
 import tornado.websocket
 
-from esphome import const
+from esphome import const, util
 from esphome.__main__ import get_serial_ports
 from esphome.helpers import mkdir_p, get_bool_env, run_system_command
-from esphome.py_compat import IS_PY2, decode_text
+from esphome.py_compat import IS_PY2, decode_text, encode_text
 from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \
     esphome_storage_path, ext_storage_path, trash_storage_path
 from esphome.util import shlex_quote
@@ -46,20 +46,23 @@ class DashboardSettings(object):
     def __init__(self):
         self.config_dir = ''
         self.password_digest = ''
+        self.username = ''
         self.using_password = False
         self.on_hassio = False
         self.cookie_secret = None
 
     def parse_args(self, args):
         self.on_hassio = args.hassio
+        password = args.password or os.getenv('PASSWORD', '')
         if not self.on_hassio:
-            self.using_password = bool(args.password)
+            self.username = args.username or os.getenv('USERNAME', '')
+            self.using_password = bool(password)
         if self.using_password:
             if IS_PY2:
-                self.password_digest = hmac.new(args.password).digest()
+                self.password_digest = hmac.new(password).digest()
             else:
-                self.password_digest = hmac.new(args.password.encode()).digest()
-        self.config_dir = args.configuration
+                self.password_digest = hmac.new(password.encode()).digest()
+        self.config_dir = args.configuration[0]
 
     @property
     def relative_url(self):
@@ -79,31 +82,20 @@ class DashboardSettings(object):
     def using_auth(self):
         return self.using_password or self.using_hassio_auth
 
-    def check_password(self, password):
+    def check_password(self, username, password):
         if not self.using_auth:
             return True
+        if username != self.username:
+            return False
 
-        if IS_PY2:
-            password = hmac.new(password).digest()
-        else:
-            password = hmac.new(password.encode()).digest()
-        return hmac.compare_digest(self.password_digest, password)
+        password_digest = hmac.new(encode_text(password)).digest()
+        return hmac.compare_digest(self.password_digest, password_digest)
 
     def rel_path(self, *args):
         return os.path.join(self.config_dir, *args)
 
     def list_yaml_files(self):
-        files = []
-        for file in os.listdir(self.config_dir):
-            if not file.endswith('.yaml'):
-                continue
-            if file.startswith('.'):
-                continue
-            if file == 'secrets.yaml':
-                continue
-            files.append(file)
-        files.sort()
-        return files
+        return util.list_yaml_files(self.config_dir)
 
 
 settings = DashboardSettings()
@@ -122,6 +114,7 @@ def template_args():
         'get_static_file_url': get_static_file_url,
         'relative_url': settings.relative_url,
         'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'),
+        'config_dir': settings.config_dir,
     }
 
 
@@ -315,6 +308,11 @@ class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
         return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"]
 
 
+class EsphomeUpdateAllHandler(EsphomeCommandWebSocket):
+    def build_command(self, json_message):
+        return ["esphome", "--dashboard", settings.config_dir, "update-all"]
+
+
 class SerialPortRequestHandler(BaseHandler):
     @authenticated
     def get(self):
@@ -374,14 +372,14 @@ def _list_dashboard_entries():
 
 
 class DashboardEntry(object):
-    def __init__(self, filename):
-        self.filename = filename
+    def __init__(self, path):
+        self.path = path
         self._storage = None
         self._loaded_storage = False
 
     @property
-    def full_path(self):  # type: () -> str
-        return os.path.join(settings.config_dir, self.filename)
+    def filename(self):
+        return os.path.basename(self.path)
 
     @property
     def storage(self):  # type: () -> Optional[StorageJSON]
@@ -402,6 +400,12 @@ class DashboardEntry(object):
             return self.filename[:-len('.yaml')]
         return self.storage.name
 
+    @property
+    def comment(self):
+        if self.storage is None:
+            return None
+        return self.storage.comment
+
     @property
     def esp_platform(self):
         if self.storage is None:
@@ -534,9 +538,12 @@ class EditRequestHandler(BaseHandler):
     @authenticated
     @bind_config
     def get(self, configuration=None):
-        # pylint: disable=no-value-for-parameter
-        with open(settings.rel_path(configuration), 'r') as f:
-            content = f.read()
+        filename = settings.rel_path(configuration)
+        content = ''
+        if os.path.isfile(filename):
+            # pylint: disable=no-value-for-parameter
+            with open(filename, 'r') as f:
+                content = f.read()
         self.write(content)
 
     @authenticated
@@ -586,16 +593,14 @@ PING_REQUEST = threading.Event()
 
 class LoginHandler(BaseHandler):
     def get(self):
-        if settings.using_hassio_auth:
-            self.render_hassio_login()
-            return
-        self.write('<html><body><form action="./login" method="post">'
-                   'Password: <input type="password" name="password">'
-                   '<input type="submit" value="Sign in">'
-                   '</form></body></html>')
+        if is_authenticated(self):
+            self.redirect('/')
+        else:
+            self.render_login_page()
 
-    def render_hassio_login(self, error=None):
-        self.render("templates/login.html", error=error, **template_args())
+    def render_login_page(self, error=None):
+        self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth,
+                    has_username=bool(settings.username), **template_args())
 
     def post_hassio_login(self):
         import requests
@@ -604,8 +609,8 @@ class LoginHandler(BaseHandler):
             'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
         }
         data = {
-            'username': str(self.get_argument('username', '')),
-            'password': str(self.get_argument('password', ''))
+            'username': decode_text(self.get_argument('username', '')),
+            'password': decode_text(self.get_argument('password', ''))
         }
         try:
             req = requests.post('http://hassio/auth', headers=headers, data=data)
@@ -616,20 +621,34 @@ class LoginHandler(BaseHandler):
         except Exception as err:  # pylint: disable=broad-except
             _LOGGER.warning("Error during Hass.io auth request: %s", err)
             self.set_status(500)
-            self.render_hassio_login(error="Internal server error")
+            self.render_login_page(error="Internal server error")
             return
         self.set_status(401)
-        self.render_hassio_login(error="Invalid username or password")
+        self.render_login_page(error="Invalid username or password")
+
+    def post_native_login(self):
+        username = decode_text(self.get_argument("username", ''))
+        password = decode_text(self.get_argument("password", ''))
+        if settings.check_password(username, password):
+            self.set_secure_cookie("authenticated", cookie_authenticated_yes)
+            self.redirect("/")
+            return
+        error_str = "Invalid username or password" if settings.username else "Invalid password"
+        self.set_status(401)
+        self.render_login_page(error=error_str)
 
     def post(self):
         if settings.using_hassio_auth:
             self.post_hassio_login()
-            return
+        else:
+            self.post_native_login()
 
-        password = str(self.get_argument("password", ''))
-        if settings.check_password(password):
-            self.set_secure_cookie("authenticated", cookie_authenticated_yes)
-        self.redirect("/")
+
+class LogoutHandler(BaseHandler):
+    @authenticated
+    def get(self):
+        self.clear_cookie("authenticated")
+        self.redirect('./login')
 
 
 _STATIC_FILE_HASHES = {}
@@ -682,6 +701,7 @@ def make_app(debug=False):
     app = tornado.web.Application([
         (rel + "", MainRequestHandler),
         (rel + "login", LoginHandler),
+        (rel + "logout", LogoutHandler),
         (rel + "logs", EsphomeLogsHandler),
         (rel + "upload", EsphomeUploadHandler),
         (rel + "compile", EsphomeCompileHandler),
@@ -690,6 +710,7 @@ def make_app(debug=False):
         (rel + "clean", EsphomeCleanHandler),
         (rel + "vscode", EsphomeVscodeHandler),
         (rel + "ace", EsphomeAceEditorHandler),
+        (rel + "update-all", EsphomeUpdateAllHandler),
         (rel + "edit", EditRequestHandler),
         (rel + "download.bin", DownloadBinaryRequestHandler),
         (rel + "serial-ports", SerialPortRequestHandler),
diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css
index 29455d9851..fddfb5cf86 100644
--- a/esphome/dashboard/static/esphome.css
+++ b/esphome/dashboard/static/esphome.css
@@ -47,6 +47,7 @@ i.very-large {
 }
 
 .log {
+  height: 100%;
   max-height: calc(100% - 56px);
   background-color: #1c1c1c;
   margin-top: 0;
@@ -131,10 +132,14 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s
 
 .select-port-container {
   margin-top: 8px;
-  margin-right: 24px;
+  margin-right: 10px;
   width: 350px;
 }
 
+#dropdown-nav-trigger {
+  margin-right: 24px;
+}
+
 .select-port-container .select-dropdown {
   color: #fff;
 }
@@ -242,3 +247,10 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s
   padding: 10px 15px;
   margin-top: 15px;
 }
+
+.card-comment {
+  margin-bottom: 8px;
+  font-size: 14px;
+  color: #444;
+  font-style: italic;
+}
diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js
index 044c2d70a3..e284690ac6 100644
--- a/esphome/dashboard/static/esphome.js
+++ b/esphome/dashboard/static/esphome.js
@@ -333,6 +333,10 @@ class LogModalElem {
     this.activeSocket.close();
   }
 
+  open(event) {
+    this._onPress(event);
+  }
+
   _onPress(event) {
     this.activeConfig = event.target.getAttribute('data-node');
     this._setupModalInstance();
@@ -570,6 +574,7 @@ const editModalElem = document.getElementById("modal-editor");
 const editorElem = editModalElem.querySelector("#editor");
 const editor = ace.edit(editorElem);
 let activeEditorConfig = null;
+let activeEditorSecrets = false;
 let aceWs = null;
 let aceValidationScheduled = false;
 let aceValidationRunning = false;
@@ -580,7 +585,6 @@ const startAceWebsocket = () => {
     if (raw.event === "line") {
       const msg = JSON.parse(raw.data);
       if (msg.type === "result") {
-        console.log(msg);
         const arr = [];
 
         for (const v of msg.validation_errors) {
@@ -681,7 +685,7 @@ editor.commands.addCommand({
 });
 
 editor.session.on('change', debounce(() => {
-  aceValidationScheduled = true;
+  aceValidationScheduled = !activeEditorSecrets;
 }, 250));
 
 setInterval(() => {
@@ -704,14 +708,21 @@ editorUploadButton.addEventListener('click', saveEditor);
 document.querySelectorAll(".action-edit").forEach((btn) => {
   btn.addEventListener('click', (e) => {
     activeEditorConfig = e.target.getAttribute('data-node');
+    activeEditorSecrets = activeEditorConfig === 'secrets.yaml';
     const modalInstance = M.Modal.getInstance(editModalElem);
     const filenameField = editModalElem.querySelector('.filename');
     editorUploadButton.setAttribute('data-node', activeEditorConfig);
+    if (activeEditorSecrets) {
+      editorUploadButton.classList.add('disabled');
+    }
     filenameField.innerHTML = activeEditorConfig;
 
+    editor.setValue("Loading configuration yaml...");
+    editor.setOption('readOnly', true);
     fetch(`./edit?configuration=${activeEditorConfig}`, {credentials: "same-origin"})
       .then(res => res.text()).then(response => {
         editor.setValue(response, -1);
+        editor.setOption('readOnly', false);
     });
 
     modalInstance.open();
@@ -742,3 +753,30 @@ jQuery.validator.addMethod("nospaces", (value, element) => {
 jQuery.validator.addMethod("lowercase", (value, element) => {
   return value === value.toLowerCase();
 }, "Name must be lowercase.");
+
+const updateAllModal = new LogModalElem({
+  name: 'update-all',
+  onPrepare: (modalElem, config) => {
+    modalElem.querySelector('.stop-logs').innerHTML = "Stop";
+    downloadButton.classList.add('disabled');
+  },
+  onProcessExit: (modalElem, code) => {
+    if (code === 0) {
+      M.toast({html: "Program exited successfully."});
+      downloadButton.classList.remove('disabled');
+    } else {
+      M.toast({html: `Program failed with code ${data.code}`});
+    }
+    modalElem.querySelector(".stop-logs").innerHTML = "Close";
+  },
+  onSocketClose: (modalElem) => {
+    M.toast({html: 'Terminated process.'});
+  },
+  dismissible: false,
+});
+updateAllModal.setup();
+
+const updateAllButton = document.getElementById('update-all-button');
+updateAllButton.addEventListener('click', (e) => {
+  updateAllModal.open(e);
+});
diff --git a/esphome/dashboard/static/fonts/LICENSE b/esphome/dashboard/static/fonts/LICENSE
new file mode 100644
index 0000000000..7a4a3ea242
--- /dev/null
+++ b/esphome/dashboard/static/fonts/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff
new file mode 100644
index 0000000000..b648a3eea2
Binary files /dev/null and b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff differ
diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2
new file mode 100644
index 0000000000..9fa2112520
Binary files /dev/null and b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 differ
diff --git a/esphome/dashboard/static/fonts/README.md b/esphome/dashboard/static/fonts/README.md
new file mode 100644
index 0000000000..34d980de08
--- /dev/null
+++ b/esphome/dashboard/static/fonts/README.md
@@ -0,0 +1,12 @@
+The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
+
+```html
+<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
+      rel="stylesheet">
+```
+
+Read more in our full usage guide:
+http://google.github.io/material-design-icons/#icon-font-for-the-web
+
+Source:
+https://github.com/google/material-design-icons
diff --git a/esphome/dashboard/static/fonts/material-icons.css b/esphome/dashboard/static/fonts/material-icons.css
new file mode 100644
index 0000000000..51f2e0a0d1
--- /dev/null
+++ b/esphome/dashboard/static/fonts/material-icons.css
@@ -0,0 +1,34 @@
+@font-face {
+  font-family: 'Material Icons';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Material Icons'),
+       local('MaterialIcons-Regular'),
+       url(MaterialIcons-Regular.woff2) format('woff2'),
+       url(MaterialIcons-Regular.woff) format('woff');
+}
+
+.material-icons {
+  font-family: 'Material Icons';
+  font-weight: normal;
+  font-style: normal;
+  font-size: 24px;  /* Preferred icon size */
+  display: inline-block;
+  line-height: 1;
+  text-transform: none;
+  letter-spacing: normal;
+  word-wrap: normal;
+  white-space: nowrap;
+  direction: ltr;
+
+  /* Support for all WebKit browsers. */
+  -webkit-font-smoothing: antialiased;
+  /* Support for Safari and Chrome. */
+  text-rendering: optimizeLegibility;
+
+  /* Support for Firefox. */
+  -moz-osx-font-smoothing: grayscale;
+
+  /* Support for IE. */
+  font-feature-settings: 'liga';
+}
diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html
index 1929f42397..0aea1c8cef 100644
--- a/esphome/dashboard/templates/index.html
+++ b/esphome/dashboard/templates/index.html
@@ -3,7 +3,7 @@
 <head>
   <meta charset="UTF-8">
   <title>ESPHome Dashboard</title>
-  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+  <link rel="stylesheet" href="{{ get_static_file_url('fonts/material-icons.css') }}">
   <link rel="stylesheet" href="{{ get_static_file_url('materialize.min.css') }}">
   <link rel="stylesheet" href="{{ get_static_file_url('materialize-stepper.min.css') }}">
   <link rel="stylesheet" href="{{ get_static_file_url('esphome.css') }}">
@@ -31,10 +31,17 @@
 <nav>
   <div class="nav-wrapper indigo">
     <a href="#" class="brand-logo left">ESPHome Dashboard</a>
+    <i class="material-icons dropdown-trigger right" id="dropdown-nav-trigger" data-target="dropdown-nav-actions">more_vert</i>
     <div class="select-port-container right" id="select-port-target">
       <select></select>
     </div>
   </div>
+
+  <ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action">
+    <li><a id="logout-button" href="{{ relative_url }}logout">Logout</a></li>
+    <li><a id="update-all-button" data-node="{{ escape(config_dir) }}">Update All</a></li>
+    <li><a id="secrets-button" class="action-edit" data-node="secrets.yaml">Secrets Editor</a></li>
+  </ul>
 </nav>
 
 {% if begin %}
@@ -72,11 +79,16 @@
               {% end %}
               <i class="material-icons right dropdown-trigger" data-target="dropdown-{{ i }}">more_vert</i>
             </span>
+            {% if entry.comment %}
+              <div class="card-comment">
+                {{ escape(entry.comment) }}
+              </div>
+            {% end %}
             <p>
               <span class="status-indicator unknown" data-node="{{ entry.filename }}">
                 <span class="status-indicator-icon"></span>
                 <span class="status-indicator-text"></span></span>.
-              Full path: <code class="inlinecode">{{ escape(entry.full_path) }}</code>
+              Full path: <code class="inlinecode">{{ escape(entry.path) }}</code>
             </p>
             {% if entry.update_available %}
               <p class="update-available" data-node="{{ entry.filename }}">
@@ -268,52 +280,70 @@
                 </optgroup>
                 <optgroup label="Other ESP32s">
                   <option value="lolin32">Wemos Lolin 32</option>
-                  <option value="esp32dev">Espressif ESP32 Dev Module</option>
                   <option value="m5stack-fire">M5Stack FIRE</option>
                   <option value="wemosbat">Wemos WiFi &amp; Bluetooth Battery</option>
-                  <option value="node32s">Aiyarafun Node32s</option>
-                  <option value="espea32">April Brother ESPea32</option>
-                  <option value="firebeetle32">DFRobot FireBeetle-ESP32</option>
-                  <option value="esp32doit-devkit-v1">Doit ESP32 Devkit v1</option>
-                  <option value="pocket_32">Dongsen Tech Pocket 32</option>
-                  <option value="espectro32">DycodeX ESPectro32</option>
-                  <option value="esp32vn-iot-uno">ESP32vn IoT Uno</option>
+                  <option value="node32s">Node32s</option>
+
+                  <option value="alksesp32">ALKS ESP32</option>
+                  <option value="bpi-bit">BPI-Bit</option>
+                  <option value="d-duino-32">D-duino-32</option>
+                  <option value="esp32-devkitlipo">OLIMEX ESP32-DevKit-LiPo</option>
+                  <option value="esp32-evb">OLIMEX ESP32-EVB</option>
+                  <option value="esp32-gateway">OLIMEX ESP32-GATEWAY</option>
+                  <option value="esp32-poe-iso">OLIMEX ESP32-PoE-ISO</option>
+                  <option value="esp32-poe">OLIMEX ESP32-PoE</option>
+                  <option value="esp32-pro">OLIMEX ESP32-PRO</option>
                   <option value="esp320">Electronic SweetPeas ESP320</option>
-                  <option value="pico32">Espressif ESP32 Pico Kit</option>
-                  <option value="odroid_esp32">Hardkernel Odroid GO</option>
-                  <option value="heltec_wifi_kit_32">Heltec WIFI Kit 32</option>
-                  <option value="heltec_wifi_lora_32">Heltec WIFI LoRa 32</option>
+                  <option value="esp32cam">AI Thinker ESP32-CAM</option>
+                  <option value="esp32dev">Espressif ESP32 Dev Module</option>
+                  <option value="esp32doit-devkit-v1">DOIT ESP32 DEVKIT V1</option>
+                  <option value="esp32thing">SparkFun ESP32 Thing</option>
+                  <option value="esp32vn-iot-uno">ESP32vn IoT Uno</option>
+                  <option value="espea32">April Brother ESPea32</option>
+                  <option value="espectro32">ESPectro32</option>
+                  <option value="espino32">ESPino32</option>
+                  <option value="firebeetle32">FireBeetle-ESP32</option>
+                  <option value="fm-devkit">ESP32 FM DevKit</option>
+                  <option value="frogboard">Frog Board ESP32</option>
+                  <option value="heltec_wifi_kit_32">Heltec WiFi Kit 32</option>
+                  <option value="heltec_wifi_lora_32">Heltec WiFi LoRa 32</option>
+                  <option value="heltec_wifi_lora_32_V2">Heltec WiFi LoRa 32 (V2)</option>
+                  <option value="heltec_wireless_stick">Heltec Wireless Stick</option>
                   <option value="hornbill32dev">Hornbill ESP32 Dev</option>
                   <option value="hornbill32minima">Hornbill ESP32 Minima</option>
                   <option value="intorobot">IntoRobot Fig</option>
-                  <option value="mhetesp32devkit">MH-ET Live ESP32 Devkit</option>
-                  <option value="mhetesp32minikit">MH-ET Live ESP32 Minikit</option>
-                  <option value="nano32">MakerAsia Nano32</option>
-                  <option value="microduino-core-esp32">Microduino Core ESP32</option>
-                  <option value="quantum">Noduino Quantum</option>
-                  <option value="esp32-evb">Olimex ESP32-EVB</option>
-                  <option value="esp32-gateway">Olimex ESP32-GATEWAY</option>
-                  <option value="esp32-pro">Olimex ESP32-PRO</option>
-                  <option value="onehorse32dev">Onehorse ESP32 Dev Module</option>
-                  <option value="alksesp32">RoboticsBrno ALKS ESP32</option>
-                  <option value="esp32thing">Sparkfun ESP32 Thing</option>
-                  <option value="ttgo-lora32-v1">TTGO LoRa32-OLED v1</option>
-                  <option value="espino32">ThaiEasyElec ESPino32</option>
-                  <option value="widora-air">Widora AIR</option>
-                  <option value="xinabox_cw02">XinaBox CW02</option>
+                  <option value="iotaap_magnolia">IoTaaP Magnolia</option>
                   <option value="iotbusio">oddWires IoT-Bus Io</option>
-                  <option value="iotbusproteus">oddWires Proteus IoT-Bus</option>
-                  <option value="nina_w10">u-blox NINA-W10 series</option>
-                  <option value="bpi-bit">BananaPi-Bit</option>
-                  <option value="d-duino-32">DSTIKE D-duino-32</option>
-                  <option value="fm-devkit">ESP32 FM DevKit</option>
-                  <option value="esp32-poe">OLIMEX ESP32-PoE</option>
-                  <option value="oroca_edubot">OROCA EduBot</option>
+                  <option value="iotbusproteus">oddWires IoT-Bus Proteus</option>
                   <option value="lopy">Pycom LoPy</option>
                   <option value="lopy4">Pycom LoPy4</option>
-                  <option value="wesp32">Silicognition wESP32</option>
+                  <option value="m5stack-grey">M5Stack GREY ESP32</option>
+                  <option value="m5stick-c">M5Stick-C</option>
+                  <option value="magicbit">MagicBit</option>
+                  <option value="mhetesp32devkit">MH ET LIVE ESP32DevKIT</option>
+                  <option value="mhetesp32minikit">MH ET LIVE ESP32MiniKit</option>
+                  <option value="microduino-core-esp32">Microduino Core ESP32</option>
+                  <option value="nano32">MakerAsia Nano32</option>
+                  <option value="nina_w10">u-blox NINA-W10 series</option>
+                  <option value="odroid_esp32">ODROID-GO</option>
+                  <option value="onehorse32dev">Onehorse ESP32 Dev Module</option>
+                  <option value="oroca_edubot">OROCA EduBot</option>
+                  <option value="pico32">ESP32 Pico Kit</option>
+                  <option value="pocket_32">Dongsen Tech Pocket 32</option>
+                  <option value="pycom_gpy">Pycom GPy</option>
+                  <option value="quantum">Noduino Quantum</option>
+                  <option value="sparkfun_lora_gateway_1-channel">SparkFun LoRa Gateway 1-Channel</option>
+                  <option value="tinypico">TinyPICO</option>
+                  <option value="ttgo-lora32-v1">TTGO LoRa32-OLED V1</option>
                   <option value="ttgo-t-beam">TTGO T-Beam</option>
+                  <option value="ttgo-t-watch">TTGO T-Watch</option>
+                  <option value="ttgo-t1">TTGO T1</option>
                   <option value="turta_iot_node">Turta IoT Node</option>
+                  <option value="vintlabs-devkit-v1">VintLabs ESP32 Devkit</option>
+                  <option value="wemos_d1_mini32">WeMos D1 MINI ESP32</option>
+                  <option value="wesp32">Silicognition wESP32</option>
+                  <option value="widora-air">Widora AIR</option>
+                  <option value="xinabox_cw02">XinaBox CW02</option>
                 </optgroup>
               </select>
             </div>
@@ -445,6 +475,16 @@
   </div>
 </div>
 
+<div id="modal-update-all" class="modal modal-fixed-footer">
+  <div class="modal-content">
+    <h4>Update All</h4>
+    <pre class="log"></pre>
+  </div>
+  <div class="modal-footer">
+    <a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a>
+  </div>
+</div>
+
 <a class="btn-floating btn-large ribbon-fab waves-effect waves-light pink accent-2" id="setup-wizard-start">
   <i class="material-icons">add</i>
 </a>
diff --git a/esphome/dashboard/templates/login.html b/esphome/dashboard/templates/login.html
index cfe682e776..414617c17f 100644
--- a/esphome/dashboard/templates/login.html
+++ b/esphome/dashboard/templates/login.html
@@ -2,10 +2,10 @@
 <html lang="en">
 <head>
   <meta charset="UTF-8">
-    <title>ESPHome Dashboard</title>
-  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+  <title>ESPHome Dashboard</title>
+  <link rel="stylesheet" href="{{ get_static_file_url('fonts/material-icons.css') }}">
   <link rel="stylesheet" href="{{ get_static_file_url('materialize.min.css') }}">
-    <link href="{{ get_static_file_url('esphome.css') }}" rel="stylesheet">
+  <link rel="stylesheet" href="{{ get_static_file_url('esphome.css') }}">
   <link rel="shortcut icon" href="{{ get_static_file_url('favicon.ico') }}">
 
   <script src="{{ get_static_file_url('materialize.min.js') }}"></script>
@@ -31,19 +31,23 @@
       <form action="./login" method="post">
         <div class="card-content">
           <span class="card-title">Enter credentials</span>
-          <p>
-            Please login using your Home Assistant credentials.
-          </p>
+          {% if hassio %}
+            <p>
+              Please login using your Home Assistant credentials.
+            </p>
+          {% end %}
           {% if error is not None %}
             <p class="error">
               {{ escape(error) }}
             </p>
           {% end %}
           <div class="row">
-            <div class="input-field col s12">
-              <label for="username">Username</label>
-              <input type="text" class="validate" name="username" id="username" />
-            </div>
+            {% if has_username or hassio %}
+              <div class="input-field col s12">
+                <label for="username">Username</label>
+                <input type="text" class="validate" name="username" id="username" />
+              </div>
+            {% end %}
             <div class="input-field col s12">
               <label for="password">Password</label>
               <input type="password" class="validate" name="password" id="password" />
diff --git a/esphome/espota2.py b/esphome/espota2.py
index 786f49dbdf..40417b9ab2 100644
--- a/esphome/espota2.py
+++ b/esphome/espota2.py
@@ -127,7 +127,8 @@ def check_error(data, expect):
                        "correct 'board' option (esp01_1m always works) and then flash over USB.")
     if dat == RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG:
         raise OTAError("Error: ESP does not have the requested flash size (wrong board). Please "
-                       "choose the correct 'board' option (esp01_1m always works) and try again.")
+                       "choose the correct 'board' option (esp01_1m always works) and try "
+                       "uploading again.")
     if dat == RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE:
         raise OTAError("Error: ESP does not have enough space to store OTA file. Please try "
                        "flashing a minimal firmware (remove everything except ota)")
@@ -299,3 +300,4 @@ def run_ota(remote_host, remote_port, password, filename):
         return run_ota_impl_(remote_host, remote_port, password, filename)
     except OTAError as err:
         _LOGGER.error(err)
+        return 1
diff --git a/esphome/helpers.py b/esphome/helpers.py
index 30a06d842f..6fd1fa2ad7 100644
--- a/esphome/helpers.py
+++ b/esphome/helpers.py
@@ -1,10 +1,11 @@
 from __future__ import print_function
 
 import codecs
+
 import logging
 import os
 
-from esphome.py_compat import char_to_byte, text_type
+from esphome.py_compat import char_to_byte, text_type, IS_PY2, encode_text
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -79,15 +80,15 @@ def run_system_command(*args):
 
 
 def mkdir_p(path):
-    import errno
-
     try:
         os.makedirs(path)
-    except OSError as exc:
-        if exc.errno == errno.EEXIST and os.path.isdir(path):
+    except OSError as err:
+        import errno
+        if err.errno == errno.EEXIST and os.path.isdir(path):
             pass
         else:
-            raise
+            from esphome.core import EsphomeError
+            raise EsphomeError(u"Error creating directories {}: {}".format(path, err))
 
 
 def is_ip_address(host):
@@ -151,17 +152,6 @@ def is_hassio():
     return get_bool_env('ESPHOME_IS_HASSIO')
 
 
-def copy_file_if_changed(src, dst):
-    src_text = read_file(src)
-    if os.path.isfile(dst):
-        dst_text = read_file(dst)
-    else:
-        dst_text = None
-    if src_text == dst_text:
-        return
-    write_file(dst, src_text)
-
-
 def walk_files(path):
     for root, _, files in os.walk(path):
         for name in files:
@@ -172,28 +162,99 @@ def read_file(path):
     try:
         with codecs.open(path, 'r', encoding='utf-8') as f_handle:
             return f_handle.read()
-    except OSError:
+    except OSError as err:
         from esphome.core import EsphomeError
-        raise EsphomeError(u"Could not read file at {}".format(path))
+        raise EsphomeError(u"Error reading file {}: {}".format(path, err))
+    except UnicodeDecodeError as err:
+        from esphome.core import EsphomeError
+        raise EsphomeError(u"Error reading file {}: {}".format(path, err))
+
+
+def _write_file(path, text):
+    import tempfile
+    directory = os.path.dirname(path)
+    mkdir_p(directory)
+
+    tmp_path = None
+    data = encode_text(text)
+    try:
+        with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle:
+            tmp_path = f_handle.name
+            f_handle.write(data)
+        # Newer tempfile implementations create the file with mode 0o600
+        os.chmod(tmp_path, 0o644)
+        if IS_PY2:
+            if os.path.exists(path):
+                os.remove(path)
+            os.rename(tmp_path, path)
+        else:
+            # If destination exists, will be overwritten
+            os.replace(tmp_path, path)
+    finally:
+        if tmp_path is not None and os.path.exists(tmp_path):
+            try:
+                os.remove(tmp_path)
+            except OSError as err:
+                _LOGGER.error("Write file cleanup failed: %s", err)
 
 
 def write_file(path, text):
     try:
-        mkdir_p(os.path.dirname(path))
-        with codecs.open(path, 'w+', encoding='utf-8') as f_handle:
-            f_handle.write(text)
+        _write_file(path, text)
     except OSError:
         from esphome.core import EsphomeError
         raise EsphomeError(u"Could not write file at {}".format(path))
 
 
-def write_file_if_changed(text, dst):
+def write_file_if_changed(path, text):
     src_content = None
-    if os.path.isfile(dst):
-        src_content = read_file(dst)
+    if os.path.isfile(path):
+        src_content = read_file(path)
     if src_content != text:
-        write_file(dst, text)
+        write_file(path, text)
+
+
+def copy_file_if_changed(src, dst):
+    import shutil
+    if file_compare(src, dst):
+        return
+    mkdir_p(os.path.dirname(dst))
+    try:
+        shutil.copy(src, dst)
+    except OSError as err:
+        from esphome.core import EsphomeError
+        raise EsphomeError(u"Error copying file {} to {}: {}".format(src, dst, err))
 
 
 def list_starts_with(list_, sub):
     return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub))
+
+
+def file_compare(path1, path2):
+    """Return True if the files path1 and path2 have the same contents."""
+    import stat
+
+    try:
+        stat1, stat2 = os.stat(path1), os.stat(path2)
+    except OSError:
+        # File doesn't exist or another error -> not equal
+        return False
+
+    if stat.S_IFMT(stat1.st_mode) != stat.S_IFREG or stat.S_IFMT(stat2.st_mode) != stat.S_IFREG:
+        # At least one of them is not a regular file (or does not exist)
+        return False
+    if stat1.st_size != stat2.st_size:
+        # Different sizes
+        return False
+
+    bufsize = 8*1024
+    # Read files in blocks until a mismatch is found
+    with open(path1, 'rb') as fh1, open(path2, 'rb') as fh2:
+        while True:
+            blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize)
+            if blob1 != blob2:
+                # Different content
+                return False
+            if not blob1:
+                # Reached end
+                return True
diff --git a/esphome/mqtt.py b/esphome/mqtt.py
index 0e00459944..e89a6d9578 100644
--- a/esphome/mqtt.py
+++ b/esphome/mqtt.py
@@ -15,6 +15,7 @@ from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \
     CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME
 from esphome.core import CORE, EsphomeError
 from esphome.helpers import color
+from esphome.py_compat import decode_text
 from esphome.util import safe_print
 
 _LOGGER = logging.getLogger(__name__)
@@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__)
 
 def initialize(config, subscriptions, on_message, username, password, client_id):
     def on_connect(client, userdata, flags, return_code):
+        _LOGGER.info("Connected to MQTT broker!")
         for topic in subscriptions:
             client.subscribe(topic)
 
@@ -94,7 +96,8 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None):
 
     def on_message(client, userdata, msg):
         time_ = datetime.now().time().strftime(u'[%H:%M:%S]')
-        message = time_ + msg.payload
+        payload = decode_text(msg.payload)
+        message = time_ + payload
         safe_print(message)
 
     return initialize(config, [topic], on_message, username, password, client_id)
diff --git a/esphome/pins.py b/esphome/pins.py
index e96dfe2e39..42c8548da4 100644
--- a/esphome/pins.py
+++ b/esphome/pins.py
@@ -36,6 +36,7 @@ ESP8266_BOARD_PINS = {
     'gen4iod': {},
     'heltec_wifi_kit_8': 'd1_mini',
     'huzzah': {'LED': 0},
+    'inventone': {},
     'modwifi': {},
     'nodemcu': {'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13,
                 'D8': 15, 'D9': 3, 'D10': 1, 'LED': 16},
@@ -84,6 +85,7 @@ ESP8266_FLASH_SIZES = {
     'gen4iod': FLASH_SIZE_512_KB,
     'heltec_wifi_kit_8': FLASH_SIZE_4_MB,
     'huzzah': FLASH_SIZE_4_MB,
+    'inventone': FLASH_SIZE_4_MB,
     'modwifi': FLASH_SIZE_2_MB,
     'nodemcu': FLASH_SIZE_4_MB,
     'nodemcuv2': FLASH_SIZE_4_MB,
@@ -118,113 +120,137 @@ ESP32_BASE_PINS = {
 }
 
 ESP32_BOARD_PINS = {
-    'alksesp32': {'D0': 40, 'D1': 41, 'D2': 15, 'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17,
-                  'D8': 5, 'D9': 18, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'A0': 32, 'A1': 33,
-                  'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15, 'L_R': 22, 'L_G': 17,
-                  'L_Y': 23, 'L_B': 5, 'L_RGB_R': 4, 'L_RGB_G': 21, 'L_RGB_B': 16, 'SW1': 15,
-                  'SW2': 2, 'SW3': 0, 'POT1': 32, 'POT2': 33, 'PIEZO1': 19, 'PIEZO2': 18,
-                  'PHOTO': 25, 'DHT_PIN': 26, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19, 'S5': 21,
-                  'SDA': 27, 'SCL': 14, 'SS': 19, 'MOSI': 21, 'MISO': 22, 'SCK': 23},
-    'bpi-bit': {'BUZZER': 25, 'BUTTON_A': 35, 'BUTTON_B': 27, 'RGB_LED': 4, 'LIGHT_SENSOR1': 36,
-                'LIGHT_SENSOR2': 39, 'TEMPERATURE_SENSOR': 34, 'MPU9250_INT': 0, 'P0': 25, 'P1': 32,
-                'P2': 33, 'P3': 13, 'P4': 15, 'P5': 35, 'P6': 12, 'P7': 14, 'P8': 16, 'P9': 17,
-                'P10': 26, 'P11': 27, 'P12': 2, 'P13': 18, 'P14': 19, 'P15': 23, 'P16': 5,
-                'P19': 22, 'P20': 21, 'DAC1': 26},
-    'd-duino-32': {'SDA': 5, 'SCL': 4, 'SS   ': 15, 'MOSI ': 13, 'MISO ': 12, 'SCK  ': 14, 'D1': 5,
-                   'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 3,
-                   'D10': 1},
+    'alksesp32': {'A0': 32, 'A1': 33, 'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15,
+                  'D0': 40, 'D1': 41, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'D2': 15,
+                  'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17, 'D8': 5, 'D9': 18, 'DHT_PIN': 26,
+                  'LED': 23, 'L_B': 5, 'L_G': 17, 'L_R': 22, 'L_RGB_B': 16, 'L_RGB_G': 21,
+                  'L_RGB_R': 4, 'L_Y': 23, 'MISO': 22, 'MOSI': 21, 'PHOTO': 25, 'PIEZO1': 19,
+                  'PIEZO2': 18, 'POT1': 32, 'POT2': 33, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19,
+                  'S5': 21, 'SCK': 23, 'SCL': 14, 'SDA': 27, 'SS': 19, 'SW1': 15, 'SW2': 2,
+                  'SW3': 0},
+    'bpi-bit': {'BUTTON_A': 35, 'BUTTON_B': 27, 'BUZZER': 25, 'LIGHT_SENSOR1': 36,
+                'LIGHT_SENSOR2': 39, 'MPU9250_INT': 0, 'P0': 25, 'P1': 32, 'P10': 26, 'P11': 27,
+                'P12': 2, 'P13': 18, 'P14': 19, 'P15': 23, 'P16': 5, 'P19': 22, 'P2': 33,
+                'P20': 21, 'P3': 13, 'P4': 15, 'P5': 35, 'P6': 12, 'P7': 14, 'P8': 16, 'P9': 17,
+                'RGB_LED': 4, 'TEMPERATURE_SENSOR': 34},
+    'd-duino-32': {'D1': 5, 'D10': 1, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13,
+                   'D8': 15, 'D9': 3, 'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SCL': 4, 'SDA': 5,
+                   'SS': 15},
     'esp-wrover-kit': {},
-    'esp32-evb': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'SS': 17, 'MOSI': 2, 'MISO': 15, 'SCK': 14},
-    'esp32-gateway': {'LED': 33, 'BUTTON': 34, 'SCL': 16, 'SDA': 17},
-    'esp32-poe': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'MOSI': 2, 'MISO': 15, 'SCK': 14},
-    'esp320': {'LED': 5, 'SDA': 2, 'SCL': 14, 'SS': 15, 'MOSI': 13, 'MISO': 12, 'SCK': 14},
+    'esp32-devkitlipo': {},
+    'esp32-evb': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13, 'SS': 17},
+    'esp32-gateway': {'BUTTON': 34, 'LED': 33, 'SCL': 16, 'SDA': 32},
+    'esp32-poe-iso': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13},
+    'esp32-poe': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13},
+    'esp32-pro': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13, 'SS': 17},
+    'esp320': {'LED': 5, 'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SCL': 14, 'SDA': 2, 'SS': 15},
+    'esp32cam': {},
     'esp32dev': {},
     'esp32doit-devkit-v1': {'LED': 2},
-    'esp32thing': {'LED': 5, 'BUTTON': 0, 'SS': 2},
+    'esp32thing': {'BUTTON': 0, 'LED': 5, 'SS': 2},
     'esp32vn-iot-uno': {},
-    'espea32': {'LED': 5, 'BUTTON': 0},
+    'espea32': {'BUTTON': 0, 'LED': 5},
     'espectro32': {'LED': 15, 'SD_SS': 33},
-    'espino32': {'LED': 16, 'BUTTON': 0},
-    'featheresp32': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5,
-                     'A0': 26, 'A1': 25, 'A2': 34, 'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15,
-                     'A9': 33, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35},
+    'espino32': {'BUTTON': 0, 'LED': 16},
+    'featheresp32': {'A0': 26, 'A1': 25, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35, 'A2': 34,
+                     'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15, 'A9': 33, 'Ax': 2,
+                     'LED': 13, 'MOSI': 18, 'RX': 16, 'SCK': 5, 'SDA': 23, 'SS': 33, 'TX': 17},
     'firebeetle32': {'LED': 2},
-    'fm-devkit': {'LED': 5, 'SW1': 4, 'SW2': 18, 'SW3': 19, 'SW4': 21, 'I2S_MCLK': 2,
-                  'I2S_LRCLK': 25, 'I2S_SCLK': 26, 'I2S_DOUT': 22, 'D0': 34, 'D1': 35, 'D2': 32,
-                  'D3': 33, 'D4': 27, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 23, 'D10': 0,
-                  'SDA': 16, 'SCL': 17},
-    'heltec_wifi_kit_32': {'LED': 25, 'BUTTON': 0, 'A1': 37, 'A2': 38},
-    'heltec_wifi_lora_32': {'LED': 25, 'BUTTON': 0, 'SDA': 4, 'SCL': 15, 'SS': 18, 'MOSI': 27,
-                            'SCK': 5, 'A1': 37, 'A2': 38, 'OLED_SCL': 15, 'OLED_SDA': 4,
-                            'OLED_RST': 16, 'LORA_SCK': 5, 'LORA_MOSI': 27, 'LORA_MISO': 19,
-                            'LORA_CS': 18, 'LORA_RST': 14, 'LORA_IRQ': 26},
-    'hornbill32dev': {'LED': 13, 'BUTTON': 0},
+    'fm-devkit': {'D0': 34, 'D1': 35, 'D10': 0, 'D2': 32, 'D3': 33, 'D4': 27, 'D5': 14, 'D6': 12,
+                  'D7': 13, 'D8': 15, 'D9': 23, 'I2S_DOUT': 22, 'I2S_LRCLK': 25, 'I2S_MCLK': 2,
+                  'I2S_SCLK': 26, 'LED': 5, 'SCL': 17, 'SDA': 16, 'SW1': 4, 'SW2': 18, 'SW3': 19,
+                  'SW4': 21},
+    'frogboard': {},
+    'heltec_wifi_kit_32': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 25, 'RST_OLED': 16,
+                           'SCL_OLED': 15, 'SDA_OLED': 4, 'Vext': 21},
+    'heltec_wifi_lora_32': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 33, 'DIO2': 32, 'LED': 25,
+                            'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, 'SCL_OLED': 15,
+                            'SDA_OLED': 4, 'SS': 18, 'Vext': 21},
+    'heltec_wifi_lora_32_V2': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 35, 'DIO2': 34, 'LED': 25,
+                               'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5,
+                               'SCL_OLED': 15, 'SDA_OLED': 4, 'SS': 18, 'Vext': 21},
+    'heltec_wireless_stick': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 35, 'DIO2': 34, 'LED': 25,
+                              'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5,
+                              'SCL_OLED': 15, 'SDA_OLED': 4, 'SS': 18, 'Vext': 21},
+    'hornbill32dev': {'BUTTON': 0, 'LED': 13},
     'hornbill32minima': {'SS': 2},
-    'intorobot': {'LED': 4, 'LED_RED': 27, 'LED_GREEN': 21, 'LED_BLUE': 22,
-                  'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, 'A2': 35,
-                  'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, 'D0': 19,
-                  'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, 'T1': 23,
-                  'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4},
+    'intorobot': {'A1': 39, 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13,
+                  'A9': 2, 'BUTTON': 0, 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5,
+                  'D6': 4, 'LED': 4, 'MISO': 17, 'MOSI': 16, 'RGB_B_BUILTIN': 22,
+                  'RGB_G_BUILTIN': 21, 'RGB_R_BUILTIN': 27, 'SCL': 19, 'SDA': 23, 'T0': 19,
+                  'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4},
+    'iotaap_magnolia': {},
+    'iotbusio': {},
+    'iotbusproteus': {},
     'lolin32': {'LED': 5},
-    'lolin_d32': {'LED': 5, 'VBAT': 35},
-    'lolin_d32_pro': {'LED': 5, 'VBAT': 35, 'TF_CS': 4, 'TS_CS': 12, 'TFT_CS': 14, 'TFT_LED': 32,
-                      'TFT_RST': 33, 'TFT_DC': 27},
-    'lopy': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 17, 'LORA_RST': 18,
-             'LORA_IRQ': 23, 'LED': 0, 'ANT_SELECT': 16, 'SDA': 12, 'SCL': 13, 'SS': 17,
-             'MOSI': 22, 'MISO': 37, 'SCK': 13, 'A1': 37, 'A2': 38},
-    'lopy4': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_IRQ': 23,
-              'LED': 0, 'ANT_SELECT': 21, 'SDA': 12, 'SCL': 13, 'SS': 18, 'MOSI': 22, 'MISO': 37,
-              'SCK': 13, 'A1': 37, 'A2': 38},
-    'm5stack-core-esp32': {'TXD2': 17, 'RXD2': 16, 'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3,
-                           'G16': 16, 'G21': 21, 'G2': 2, 'G12': 12, 'G15': 15, 'G35': 35,
-                           'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, 'G17': 17, 'G22': 22, 'G5': 5,
-                           'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, 'ADC2': 36},
-    'm5stack-fire': {'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, 'G16': 16, 'G21': 21, 'G2': 2,
-                     'G12': 12, 'G15': 15, 'G35': 35, 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1,
-                     'G17': 17, 'G22': 22, 'G5': 5, 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35,
-                     'ADC2': 36},
+    'lolin_d32': {'LED': 5, '_VBAT': 35},
+    'lolin_d32_pro': {'LED': 5, '_VBAT': 35},
+    'lopy': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13,
+             'SDA': 12, 'SS': 17},
+    'lopy4': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13,
+              'SDA': 12, 'SS': 18},
+    'm5stack-core-esp32': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13,
+                           'G15': 15, 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2,
+                           'G21': 21, 'G22': 22, 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3,
+                           'G34': 34, 'G35': 35, 'G36': 36, 'G5': 5, 'RXD2': 16, 'TXD2': 17},
+    'm5stack-fire': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, 'G15': 15,
+                     'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, 'G21': 21, 'G22': 22,
+                     'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, 'G34': 34, 'G35': 35, 'G36': 36,
+                     'G5': 5},
+    'm5stack-grey': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, 'G15': 15,
+                     'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, 'G21': 21, 'G22': 22,
+                     'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, 'G34': 34, 'G35': 35, 'G36': 36,
+                     'G5': 5, 'RXD2': 16, 'TXD2': 17},
+    'm5stick-c': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G10': 10, 'G26': 26, 'G32': 32, 'G33': 33,
+                  'G36': 36, 'G37': 37, 'G39': 39, 'G9': 9, 'MISO': 36, 'MOSI': 15, 'SCK': 13,
+                  'SCL': 33, 'SDA': 32},
+    'magicbit': {'BLUE_LED': 17, 'BUZZER': 25, 'GREEN_LED': 16, 'LDR': 36, 'LED': 16,
+                 'LEFT_BUTTON': 35, 'MOTOR1A': 27, 'MOTOR1B': 18, 'MOTOR2A': 16, 'MOTOR2B': 17,
+                 'POT': 39, 'RED_LED': 27, 'RIGHT_PUTTON': 34, 'YELLOW_LED': 18},
     'mhetesp32devkit': {'LED': 2},
     'mhetesp32minikit': {'LED': 2},
-    'microduino-core-esp32': {'SDA': 22, 'SCL': 21, 'SDA1': 12, 'SCL1': 13, 'A0': 12, 'A1': 13,
-                              'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'A10': 25,
-                              'A11': 26, 'A12': 27, 'A13': 14, 'D0': 3, 'D1': 1, 'D2': 16, 'D3': 17,
-                              'D4': 32, 'D5': 33, 'D6': 25, 'D7': 26, 'D8': 27, 'D9': 14, 'D10': 5,
-                              'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12, 'D15': 13, 'D16': 15,
-                              'D17': 4, 'D18': 22, 'D19': 21, 'D20': 38, 'D21': 37},
-    'nano32': {'LED': 16, 'BUTTON': 0},
-    'nina_w10': {'LED_GREEN': 33, 'LED_RED': 23, 'LED_BLUE': 21, 'SW1': 33, 'SW2': 27, 'SDA': 12,
-                 'SCL': 13, 'D0': 3, 'D1': 1, 'D2': 26, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22,
-                 'D7': 0, 'D8': 15, 'D9': 14, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13,
-                 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D20': 36, 'D21': 39},
+    'microduino-core-esp32': {'A0': 12, 'A1': 13, 'A10': 25, 'A11': 26, 'A12': 27, 'A13': 14,
+                              'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'D0': 3,
+                              'D1': 1, 'D10': 5, 'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12,
+                              'D15': 13, 'D16': 15, 'D17': 4, 'D18': 22, 'D19': 21, 'D2': 16,
+                              'D20': 38, 'D21': 37, 'D3': 17, 'D4': 32, 'D5': 33, 'D6': 25,
+                              'D7': 26, 'D8': 27, 'D9': 14, 'SCL': 21, 'SCL1': 13, 'SDA': 22,
+                              'SDA1': 12},
+    'nano32': {'BUTTON': 0, 'LED': 16},
+    'nina_w10': {'D0': 3, 'D1': 1, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13,
+                 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D2': 26, 'D20': 36,
+                 'D21': 39, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22, 'D7': 0, 'D8': 15, 'D9': 14,
+                 'LED_BLUE': 21, 'LED_GREEN': 33, 'LED_RED': 23, 'SCL': 13, 'SDA': 12, 'SW1': 33,
+                 'SW2': 27},
     'node32s': {},
-    'nodemcu-32s': {'LED': 2, 'BUTTON': 0},
-    'odroid_esp32': {'LED': 2, 'SDA': 15, 'SCL': 4, 'SS': 22, 'ADC1': 35, 'ADC2': 36},
-    'onehorse32dev': {'LED': 5, 'BUTTON': 0, 'A1': 37, 'A2': 38},
-    'oroca_edubot': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5,
-                     'A0': 34, 'A1': 39, 'A2': 36, 'A3': 33, 'D0': 4, 'D1': 16, 'D2': 17, 'D3': 22,
-                     'D4': 23, 'D5': 5, 'D6': 18, 'D7': 19, 'D8': 33, 'VBAT': 35},
+    'nodemcu-32s': {'BUTTON': 0, 'LED': 2},
+    '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': {'A0': 34, 'A1': 39, 'A2': 36, 'A3': 33, 'D0': 4, 'D1': 16, 'D2': 17,
+                     'D3': 22, 'D4': 23, 'D5': 5, 'D6': 18, 'D7': 19, 'D8': 33, 'LED': 13,
+                     'MOSI': 18, 'RX': 16, 'SCK': 5, 'SDA': 23, 'SS': 2, 'TX': 17, 'VBAT': 35},
     'pico32': {},
     'pocket_32': {'LED': 16},
+    'pycom_gpy': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13,
+                  'SDA': 12, 'SS': 17},
     'quantum': {},
-    'ttgo-lora32-v1': {'LED': 2, 'BUTTON': 0, 'SS': 18, 'MOSI': 27, 'SCK': 5, 'A1': 37, 'A2': 38,
-                       'OLED_SDA': 4, 'OLED_SCL': 15, 'OLED_RST': 16, 'LORA_SCK': 5,
-                       'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_RST': 14,
-                       'LORA_IRQ': 26},
-    'ttgo-t-beam': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_RST': 23,
-                    'LORA_IRQ': 26, 'LORA_IO1': 33, 'LORA_IO2': 32, 'SS': 18, 'MOSI': 27, 'SCK': 5,
-                    'T8': 32, 'T9': 33, 'DAC2': 25},
-    'turta_iot_node': {'LED': 13, 'TX': 10, 'RX': 9, 'SDA': 23, 'SS': 21, 'MOSI': 18, 'SCK': 5,
-                       'A0': 4, 'A1': 25, 'A2': 26, 'A3': 27, 'A8': 38, 'T1': 25, 'T2': 26,
-                       'T3': 27, 'T4': 32, 'T5': 33, 'T6': 34, 'T7': 35, 'T8': 22, 'T9': 23,
-                       'T10': 10, 'T11': 9, 'T12': 21, 'T13': 5, 'T14': 18, 'T15': 19,
-                       'T16': 37, 'T17': 14, 'T18': 2, 'T19': 38},
-    'wemosbat': 'pocket_32',
-    'wesp32': {'SCL': 4, 'SDA': 2, 'MISO': 32, 'ETH_PHY_ADDR': 0, 'ETH_PHY_MDC': 16,
-               'ETH_PHY_MDIO': 17},
-    'widora-air': {'LED': 25, 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39,
-                   'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2,
-                   'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19,
-                   'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4},
+    'sparkfun_lora_gateway_1-channel': {'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SS': 16},
+    'tinypico': {},
+    'ttgo-lora32-v1': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 2, 'MOSI': 27, 'SCK': 5, 'SS': 18},
+    '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},
+    'turta_iot_node': {},
+    'vintlabs-devkit-v1': {'LED': 2, 'PWM0': 12, 'PWM1': 13, 'PWM2': 14, 'PWM3': 15, 'PWM4': 16,
+                           'PWM5': 17, 'PWM6': 18, 'PWM7': 19},
+    'wemos_d1_mini32': {'D0': 26, 'D1': 22, 'D2': 21, 'D3': 17, 'D4': 16, 'D5': 18, 'D6': 19,
+                        'D7': 23, 'D8': 5, 'LED': 2, 'RXD': 3, 'TXD': 1, '_VBAT': 35},
+    'wemosbat': {'LED': 16},
+    'wesp32': {'MISO': 32, 'SCL': 4, 'SDA': 15},
+    'widora-air': {'A1': 39, 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13,
+                   'A9': 2, 'BUTTON': 0, 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16,
+                   'D5': 5, 'D6': 4, 'LED': 25, 'MISO': 17, 'MOSI': 16, 'SCL': 19, 'SDA': 23,
+                   'T0': 19, 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4},
     'xinabox_cw02': {'LED': 27},
 }
 
diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py
index 1656f1ad33..5cc4fad998 100644
--- a/esphome/platformio_api.py
+++ b/esphome/platformio_api.py
@@ -7,7 +7,7 @@ import re
 import subprocess
 
 from esphome.core import CORE
-from esphome.py_compat import IS_PY2
+from esphome.py_compat import decode_text
 from esphome.util import run_external_command, run_external_process
 
 _LOGGER = logging.getLogger(__name__)
@@ -18,15 +18,13 @@ def patch_structhash():
     # removed/added. This might have unintended consequences, but this improves compile
     # times greatly when adding/removing components and a simple clean build solves
     # all issues
-    # pylint: disable=no-member,no-name-in-module
-    from platformio.commands import run
-    from platformio import util
-    from platformio.util import get_project_dir
-    from os.path import join, isdir, getmtime, isfile
+    from platformio.commands.run import helpers, command
+    from os.path import join, isdir, getmtime
     from os import makedirs
 
-    def patched_clean_build_dir(build_dir):
-        structhash_file = join(build_dir, "structure.hash")
+    def patched_clean_build_dir(build_dir, *args):
+        from platformio import util
+        from platformio.project.helpers import get_project_dir
         platformio_ini = join(get_project_dir(), "platformio.ini")
 
         # if project's config is modified
@@ -36,27 +34,33 @@ def patch_structhash():
         if not isdir(build_dir):
             makedirs(build_dir)
 
-        proj_hash = run.calculate_project_hash()
-
-        # check project structure
-        if isdir(build_dir) and isfile(structhash_file):
-            with open(structhash_file) as f:
-                if f.read() == proj_hash:
-                    return
-
-        with open(structhash_file, "w") as f:
-            f.write(proj_hash)
-
     # pylint: disable=protected-access
-    orig = run._clean_build_dir
+    helpers.clean_build_dir = patched_clean_build_dir
+    command.clean_build_dir = patched_clean_build_dir
 
-    def patched_safe(*args, **kwargs):
-        try:
-            return patched_clean_build_dir(*args, **kwargs)
-        except Exception:  # pylint: disable=broad-except
-            return orig(*args, **kwargs)
 
-    run._clean_build_dir = patched_safe
+IGNORE_LIB_WARNINGS = r'(?:' + '|'.join(['Hash', 'Update']) + r')'
+FILTER_PLATFORMIO_LINES = [
+    r'Verbose mode can be enabled via `-v, --verbose` option.*',
+    r'CONFIGURATION: https://docs.platformio.org/.*',
+    r'PLATFORM: .*',
+    r'DEBUG: Current.*',
+    r'PACKAGES: .*',
+    r'LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*',
+    r'LDF Modes: Finder ~ chain, Compatibility ~ soft.*',
+    r'Looking for ' + IGNORE_LIB_WARNINGS + r' library in registry',
+    r"Warning! Library `.*'" + IGNORE_LIB_WARNINGS +
+    r".*` has not been found in PlatformIO Registry.",
+    r"You can ignore this message, if `.*" + IGNORE_LIB_WARNINGS + r".*` is a built-in library.*",
+    r'Scanning dependencies...',
+    r"Found \d+ compatible libraries",
+    r'Memory Usage -> http://bit.ly/pio-memory-usage',
+    r'esptool.py v.*',
+    r"Found: https://platformio.org/lib/show/.*",
+    r"Using cache: .*",
+    r'Installing dependencies',
+    r'.* @ .* is already installed',
+]
 
 
 def run_platformio_cli(*args, **kwargs):
@@ -65,18 +69,16 @@ def run_platformio_cli(*args, **kwargs):
     os.environ["PLATFORMIO_LIBDEPS_DIR"] = os.path.abspath(CORE.relative_piolibdeps_path())
     cmd = ['platformio'] + list(args)
 
-    if os.environ.get('ESPHOME_USE_SUBPROCESS') is None:
-        import platformio.__main__
-        try:
-            if IS_PY2:
-                patch_structhash()
-        except Exception:  # pylint: disable=broad-except
-            # Ignore when patch fails
-            pass
-        return run_external_command(platformio.__main__.main,
-                                    *cmd, **kwargs)
+    if not CORE.verbose:
+        kwargs['filter_lines'] = FILTER_PLATFORMIO_LINES
 
-    return run_external_process(*cmd, **kwargs)
+    if os.environ.get('ESPHOME_USE_SUBPROCESS') is not None:
+        return run_external_process(*cmd, **kwargs)
+
+    import platformio.__main__
+    patch_structhash()
+    return run_external_command(platformio.__main__.main,
+                                *cmd, **kwargs)
 
 
 def run_platformio_cli_run(config, verbose, *args, **kwargs):
@@ -98,6 +100,7 @@ def run_upload(config, verbose, port):
 def run_idedata(config):
     args = ['-t', 'idedata']
     stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True)
+    stdout = decode_text(stdout)
     match = re.search(r'{.*}', stdout)
     if match is None:
         return IDEData(None)
diff --git a/esphome/py_compat.py b/esphome/py_compat.py
index 6833a55801..6cdaa5b047 100644
--- a/esphome/py_compat.py
+++ b/esphome/py_compat.py
@@ -1,5 +1,6 @@
 import functools
 import sys
+import codecs
 
 PYTHON_MAJOR = sys.version_info[0]
 IS_PY2 = PYTHON_MAJOR == 2
@@ -75,15 +76,14 @@ def indexbytes(buf, i):
         return ord(buf[i])
 
 
-if IS_PY2:
-    def decode_text(data, encoding='utf-8', errors='strict'):
-        # type: (str, str, str) -> unicode
-        if isinstance(data, unicode):
-            return data
-        return unicode(data, encoding=encoding, errors=errors)
-else:
-    def decode_text(data, encoding='utf-8', errors='strict'):
-        # type: (bytes, str, str) -> str
-        if isinstance(data, str):
-            return data
-        return data.decode(encoding=encoding, errors=errors)
+def decode_text(data, encoding='utf-8', errors='strict'):
+    if isinstance(data, text_type):
+        return data
+    return codecs.decode(data, encoding, errors)
+
+
+def encode_text(data, encoding='utf-8', errors='strict'):
+    if isinstance(data, binary_type):
+        return data
+
+    return codecs.encode(data, encoding, errors)
diff --git a/esphome/storage_json.py b/esphome/storage_json.py
index b04f056f11..0305b59ef5 100644
--- a/esphome/storage_json.py
+++ b/esphome/storage_json.py
@@ -7,12 +7,13 @@ import os
 
 from esphome import const
 from esphome.core import CORE
-from esphome.helpers import mkdir_p
+from esphome.helpers import mkdir_p, write_file_if_changed
 
 # pylint: disable=unused-import, wrong-import-order
 from esphome.core import CoreType  # noqa
 from typing import Any, Dict, Optional  # noqa
 
+from esphome.py_compat import text_type
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -35,7 +36,7 @@ def trash_storage_path(base_path):  # type: (str) -> str
 
 # pylint: disable=too-many-instance-attributes
 class StorageJSON(object):
-    def __init__(self, storage_version, name, esphome_version,
+    def __init__(self, storage_version, name, comment, esphome_version,
                  src_version, arduino_version, address, esp_platform, board, build_path,
                  firmware_bin_path, loaded_integrations):
         # Version of the storage JSON schema
@@ -43,6 +44,8 @@ class StorageJSON(object):
         self.storage_version = storage_version  # type: int
         # The name of the node
         self.name = name  # type: str
+        # The comment of the node
+        self.comment = comment  # type: str
         # The esphome version this was compiled with
         self.esphome_version = esphome_version  # type: str
         # The version of the file in src/main.cpp - Used to migrate the file
@@ -69,6 +72,7 @@ class StorageJSON(object):
         return {
             'storage_version': self.storage_version,
             'name': self.name,
+            'comment': self.comment,
             'esphome_version': self.esphome_version,
             'src_version': self.src_version,
             'arduino_version': self.arduino_version,
@@ -85,14 +89,14 @@ class StorageJSON(object):
 
     def save(self, path):
         mkdir_p(os.path.dirname(path))
-        with codecs.open(path, 'w', encoding='utf-8') as f_handle:
-            f_handle.write(self.to_json())
+        write_file_if_changed(path, self.to_json())
 
     @staticmethod
     def from_esphome_core(esph, old):  # type: (CoreType, Optional[StorageJSON]) -> StorageJSON
         return StorageJSON(
             storage_version=1,
             name=esph.name,
+            comment=esph.comment,
             esphome_version=const.__version__,
             src_version=1,
             arduino_version=esph.arduino_version,
@@ -110,6 +114,7 @@ class StorageJSON(object):
         return StorageJSON(
             storage_version=1,
             name=name,
+            comment=None,
             esphome_version=const.__version__,
             src_version=1,
             arduino_version=None,
@@ -124,10 +129,10 @@ class StorageJSON(object):
     @staticmethod
     def _load_impl(path):  # type: (str) -> Optional[StorageJSON]
         with codecs.open(path, 'r', encoding='utf-8') as f_handle:
-            text = f_handle.read()
-        storage = json.loads(text, encoding='utf-8')
+            storage = json.load(f_handle)
         storage_version = storage['storage_version']
         name = storage.get('name')
+        comment = storage.get('comment')
         esphome_version = storage.get('esphome_version', storage.get('esphomeyaml_version'))
         src_version = storage.get('src_version')
         arduino_version = storage.get('arduino_version')
@@ -137,7 +142,7 @@ class StorageJSON(object):
         build_path = storage.get('build_path')
         firmware_bin_path = storage.get('firmware_bin_path')
         loaded_integrations = storage.get('loaded_integrations', [])
-        return StorageJSON(storage_version, name, esphome_version,
+        return StorageJSON(storage_version, name, comment, esphome_version,
                            src_version, arduino_version, address, esp_platform, board, build_path,
                            firmware_bin_path, loaded_integrations)
 
@@ -188,15 +193,12 @@ class EsphomeStorageJSON(object):
         return json.dumps(self.as_dict(), indent=2) + u'\n'
 
     def save(self, path):  # type: (str) -> None
-        mkdir_p(os.path.dirname(path))
-        with codecs.open(path, 'w', encoding='utf-8') as f_handle:
-            f_handle.write(self.to_json())
+        write_file_if_changed(path, self.to_json())
 
     @staticmethod
     def _load_impl(path):  # type: (str) -> Optional[EsphomeStorageJSON]
         with codecs.open(path, 'r', encoding='utf-8') as f_handle:
-            text = f_handle.read()
-        storage = json.loads(text, encoding='utf-8')
+            storage = json.load(f_handle)
         storage_version = storage['storage_version']
         cookie_secret = storage.get('cookie_secret')
         last_update_check = storage.get('last_update_check')
@@ -215,7 +217,7 @@ class EsphomeStorageJSON(object):
     def get_default():  # type: () -> EsphomeStorageJSON
         return EsphomeStorageJSON(
             storage_version=1,
-            cookie_secret=binascii.hexlify(os.urandom(64)),
+            cookie_secret=text_type(binascii.hexlify(os.urandom(64))),
             last_update_check=None,
             remote_version=None,
         )
diff --git a/esphome/util.py b/esphome/util.py
index 0ae0b9e32e..098d5e52da 100644
--- a/esphome/util.py
+++ b/esphome/util.py
@@ -3,12 +3,13 @@ from __future__ import print_function
 import collections
 import io
 import logging
+import os
 import re
 import subprocess
 import sys
 
 from esphome import const
-from esphome.py_compat import IS_PY2
+from esphome.py_compat import IS_PY2, decode_text, text_type
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -87,24 +88,67 @@ def shlex_quote(s):
     return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
 
 
+ANSI_ESCAPE = re.compile(r'\033[@-_][0-?]*[ -/]*[@-~]')
+
+
 class RedirectText(object):
-    def __init__(self, out):
+    def __init__(self, out, filter_lines=None):
         self._out = out
+        if filter_lines is None:
+            self._filter_pattern = None
+        else:
+            pattern = r'|'.join(r'(?:' + pattern + r')' for pattern in filter_lines)
+            self._filter_pattern = re.compile(pattern)
+        self._line_buffer = ''
 
     def __getattr__(self, item):
         return getattr(self._out, item)
 
-    def write(self, s):
+    def _write_color_replace(self, s):
         from esphome.core import CORE
 
         if CORE.dashboard:
-            try:
-                s = s.replace('\033', '\\033')
-            except UnicodeEncodeError:
-                pass
-
+            # With the dashboard, we must create a little hack to make color output
+            # work. The shell we create in the dashboard is not a tty, so python removes
+            # all color codes from the resulting stream. We just convert them to something
+            # we can easily recognize later here.
+            s = s.replace('\033', '\\033')
         self._out.write(s)
 
+    def write(self, s):
+        # s is usually a text_type already (self._out is of type TextIOWrapper)
+        # However, s is sometimes also a bytes object in python3. Let's make sure it's a
+        # text_type
+        # If the conversion fails, we will create an exception, which is okay because we won't
+        # be able to print it anyway.
+        text = decode_text(s)
+        assert isinstance(text, text_type)
+
+        if self._filter_pattern is not None:
+            self._line_buffer += text
+            lines = self._line_buffer.splitlines(True)
+            for line in lines:
+                if '\n' not in line and '\r' not in line:
+                    # Not a complete line, set line buffer
+                    self._line_buffer = line
+                    break
+                self._line_buffer = ''
+
+                line_without_ansi = ANSI_ESCAPE.sub('', line)
+                line_without_end = line_without_ansi.rstrip()
+                if self._filter_pattern.match(line_without_end) is not None:
+                    # Filter pattern matched, ignore the line
+                    continue
+
+                self._write_color_replace(line)
+        else:
+            self._write_color_replace(text)
+
+        # write() returns the number of characters written
+        # Let's print the number of characters of the original string in order to not confuse
+        # any caller.
+        return len(s)
+
     # pylint: disable=no-self-use
     def isatty(self):
         return True
@@ -119,14 +163,15 @@ def run_external_command(func, *cmd, **kwargs):
     full_cmd = u' '.join(shlex_quote(x) for x in cmd)
     _LOGGER.info(u"Running:  %s", full_cmd)
 
+    filter_lines = kwargs.get('filter_lines')
     orig_stdout = sys.stdout
-    sys.stdout = RedirectText(sys.stdout)
+    sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
     orig_stderr = sys.stderr
-    sys.stderr = RedirectText(sys.stderr)
+    sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
 
     capture_stdout = kwargs.get('capture_stdout', False)
     if capture_stdout:
-        cap_stdout = sys.stdout = io.BytesIO()
+        cap_stdout = sys.stdout = io.StringIO()
 
     try:
         sys.argv = list(cmd)
@@ -154,14 +199,15 @@ def run_external_command(func, *cmd, **kwargs):
 def run_external_process(*cmd, **kwargs):
     full_cmd = u' '.join(shlex_quote(x) for x in cmd)
     _LOGGER.info(u"Running:  %s", full_cmd)
+    filter_lines = kwargs.get('filter_lines')
 
     capture_stdout = kwargs.get('capture_stdout', False)
     if capture_stdout:
         sub_stdout = io.BytesIO()
     else:
-        sub_stdout = RedirectText(sys.stdout)
+        sub_stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
 
-    sub_stderr = RedirectText(sys.stderr)
+    sub_stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
 
     try:
         return subprocess.call(cmd,
@@ -207,3 +253,16 @@ class OrderedDict(collections.OrderedDict):
                 root[1] = first[0] = link
         else:
             super(OrderedDict, self).move_to_end(key, last=last)  # pylint: disable=no-member
+
+
+def list_yaml_files(folder):
+    files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)])
+    files.sort()
+    return files
+
+
+def filter_yaml_files(files):
+    files = [f for f in files if os.path.splitext(f)[1] == '.yaml']
+    files = [f for f in files if os.path.basename(f) != 'secrets.yaml']
+    files = [f for f in files if not os.path.basename(f).startswith('.')]
+    return files
diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py
index 13f4c5d6b8..09fa1a6756 100644
--- a/esphome/voluptuous_schema.py
+++ b/esphome/voluptuous_schema.py
@@ -21,8 +21,8 @@ def ensure_multiple_invalid(err):
 # pylint: disable=protected-access, unidiomatic-typecheck
 class _Schema(vol.Schema):
     """Custom cv.Schema that prints similar keys on error."""
-    def __init__(self, schema, extra=vol.PREVENT_EXTRA, extra_schemas=None):
-        super(_Schema, self).__init__(schema, extra=extra)
+    def __init__(self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None):
+        super(_Schema, self).__init__(schema, required=required, extra=extra)
         # List of extra schemas to apply after validation
         # Should be used sparingly, as it's not a very voluptuous-way/clean way of
         # doing things.
diff --git a/esphome/vscode.py b/esphome/vscode.py
index f0fa4ef52a..151bfb5281 100644
--- a/esphome/vscode.py
+++ b/esphome/vscode.py
@@ -59,7 +59,7 @@ def read_config(args):
         CORE.ace = args.ace
         f = data['file']
         if CORE.ace:
-            CORE.config_path = os.path.join(args.configuration, f)
+            CORE.config_path = os.path.join(args.configuration[0], f)
         else:
             CORE.config_path = data['file']
         vs = VSCodeResult()
diff --git a/esphome/wizard.py b/esphome/wizard.py
index ac77915497..67ec2c8960 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -1,13 +1,14 @@
 from __future__ import print_function
 
-import codecs
 import os
+import random
+import string
 import unicodedata
 
 import voluptuous as vol
 
 import esphome.config_validation as cv
-from esphome.helpers import color, get_bool_env
+from esphome.helpers import color, get_bool_env, write_file
 # pylint: disable=anomalous-backslash-in-string
 from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS
 from esphome.py_compat import safe_input, text_type
@@ -52,6 +53,13 @@ wifi:
   ssid: "{ssid}"
   password: "{psk}"
 
+  # Enable fallback hotspot (captive portal) in case wifi connection fails
+  ap:
+    ssid: "{fallback_name}"
+    password: "{fallback_psk}"
+
+captive_portal:
+
 # Enable logging
 logger:
 
@@ -65,6 +73,14 @@ def sanitize_double_quotes(value):
 
 
 def wizard_file(**kwargs):
+    letters = string.ascii_letters + string.digits
+    ap_name_base = kwargs['name'].replace('_', ' ').title()
+    ap_name = "{} Fallback Hotspot".format(ap_name_base)
+    if len(ap_name) > 32:
+        ap_name = ap_name_base
+    kwargs['fallback_name'] = ap_name
+    kwargs['fallback_psk'] = ''.join(random.choice(letters) for _ in range(12))
+
     config = BASE_CONFIG.format(**kwargs)
 
     if kwargs['password']:
@@ -87,8 +103,7 @@ def wizard_write(path, **kwargs):
         kwargs['platform'] = 'ESP8266' if board in ESP8266_BOARD_PINS else 'ESP32'
     platform = kwargs['platform']
 
-    with codecs.open(path, 'w', 'utf-8') as f_handle:
-        f_handle.write(wizard_file(**kwargs))
+    write_file(path, wizard_file(**kwargs))
     storage = StorageJSON.from_wizard(name, name + '.local', platform, board)
     storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path))
     storage.save(storage_path)
@@ -117,8 +132,8 @@ def default_input(text, default):
 
 
 # From https://stackoverflow.com/a/518232/8924614
-def strip_accents(string):
-    return u''.join(c for c in unicodedata.normalize('NFD', text_type(string))
+def strip_accents(value):
+    return u''.join(c for c in unicodedata.normalize('NFD', text_type(value))
                     if unicodedata.category(c) != 'Mn')
 
 
diff --git a/esphome/writer.py b/esphome/writer.py
index bda3f09df2..4c2e03569f 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -8,7 +8,8 @@ from esphome.config import iter_components
 from esphome.const import CONF_BOARD_FLASH_MODE, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, \
     HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__
 from esphome.core import CORE, EsphomeError
-from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files
+from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files, \
+    copy_file_if_changed
 from esphome.storage_json import StorageJSON, storage_path
 
 _LOGGER = logging.getLogger(__name__)
@@ -112,7 +113,7 @@ def migrate_src_version_0_to_1():
                           "auto-generated again.", main_cpp, main_cpp)
         _LOGGER.info("Migration: Added include section to %s", main_cpp)
 
-    write_file_if_changed(content, main_cpp)
+    write_file_if_changed(main_cpp, content)
 
 
 def migrate_src_version(old, new):
@@ -171,9 +172,7 @@ def format_ini(data):
 
 
 def gather_lib_deps():
-    lib_deps_l = [x.as_lib_dep for x in CORE.libraries]
-    lib_deps_l.sort()
-    return lib_deps_l
+    return [x.as_lib_dep for x in CORE.libraries]
 
 
 def gather_build_flags():
@@ -193,7 +192,7 @@ def get_ini_content():
         'framework': 'arduino',
         'lib_deps': lib_deps + ['${common.lib_deps}'],
         'build_flags': build_flags + ['${common.build_flags}'],
-        'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 115200),
+        'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 460800),
     }
 
     if CORE.is_esp32:
@@ -253,7 +252,7 @@ def write_platformio_ini(content):
         content_format = INI_BASE_FORMAT
     full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + '\n' + content
     full_file += INI_AUTO_GENERATE_END + content_format[1]
-    write_file_if_changed(full_file, path)
+    write_file_if_changed(path, full_file)
 
 
 def write_platformio_project():
@@ -287,7 +286,6 @@ or use the custom_components folder.
 
 
 def copy_src_tree():
-    import filecmp
     import shutil
 
     source_files = {}
@@ -323,9 +321,7 @@ def copy_src_tree():
             os.remove(path)
         else:
             src_path = source_files_copy.pop(target)
-            if not filecmp.cmp(path, src_path):
-                # Files are not same, copy
-                shutil.copy(src_path, path)
+            copy_file_if_changed(src_path, path)
 
     # Now copy new files
     for target, src_path in source_files_copy.items():
@@ -334,14 +330,14 @@ def copy_src_tree():
         shutil.copy(src_path, dst_path)
 
     # Finally copy defines
-    write_file_if_changed(generate_defines_h(),
-                          CORE.relative_src_path('esphome', 'core', 'defines.h'))
-    write_file_if_changed(ESPHOME_README_TXT,
-                          CORE.relative_src_path('esphome', 'README.txt'))
-    write_file_if_changed(ESPHOME_H_FORMAT.format(include_s),
-                          CORE.relative_src_path('esphome.h'))
-    write_file_if_changed(VERSION_H_FORMAT.format(__version__),
-                          CORE.relative_src_path('esphome', 'core', 'version.h'))
+    write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'defines.h'),
+                          generate_defines_h())
+    write_file_if_changed(CORE.relative_src_path('esphome', 'README.txt'),
+                          ESPHOME_README_TXT)
+    write_file_if_changed(CORE.relative_src_path('esphome.h'),
+                          ESPHOME_H_FORMAT.format(include_s))
+    write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'version.h'),
+                          VERSION_H_FORMAT.format(__version__))
 
 
 def generate_defines_h():
@@ -367,7 +363,7 @@ def write_cpp(code_s):
     full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END
     full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END
     full_file += code_format[2]
-    write_file_if_changed(full_file, path)
+    write_file_if_changed(path, full_file)
 
 
 def clean_build():
diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py
index 30e8bb82de..fb04d7d5b0 100644
--- a/esphome/yaml_util.py
+++ b/esphome/yaml_util.py
@@ -14,7 +14,7 @@ from esphome import core
 from esphome.config_helpers import read_config_file
 from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange
 from esphome.py_compat import text_type, IS_PY2
-from esphome.util import OrderedDict
+from esphome.util import OrderedDict, filter_yaml_files
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -260,12 +260,12 @@ class ESPHomeLoader(yaml.SafeLoader):  # pylint: disable=too-many-ancestors
 
     @_add_data_ref
     def construct_include_dir_list(self, node):
-        files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
+        files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
         return [_load_yaml_internal(f) for f in files]
 
     @_add_data_ref
     def construct_include_dir_merge_list(self, node):
-        files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
+        files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
         merged_list = []
         for fname in files:
             loaded_yaml = _load_yaml_internal(fname)
@@ -275,7 +275,7 @@ class ESPHomeLoader(yaml.SafeLoader):  # pylint: disable=too-many-ancestors
 
     @_add_data_ref
     def construct_include_dir_named(self, node):
-        files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
+        files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
         mapping = OrderedDict()
         for fname in files:
             filename = os.path.splitext(os.path.basename(fname))[0]
@@ -284,7 +284,7 @@ class ESPHomeLoader(yaml.SafeLoader):  # pylint: disable=too-many-ancestors
 
     @_add_data_ref
     def construct_include_dir_merge_named(self, node):
-        files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
+        files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml'))
         mapping = OrderedDict()
         for fname in files:
             loaded_yaml = _load_yaml_internal(fname)
@@ -297,12 +297,6 @@ class ESPHomeLoader(yaml.SafeLoader):  # pylint: disable=too-many-ancestors
         return Lambda(text_type(node.value))
 
 
-def _filter_yaml_files(files):
-    files = [f for f in files if os.path.basename(f) != SECRET_YAML]
-    files = [f for f in files if not os.path.basename(f).startswith('.')]
-    return files
-
-
 ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int)
 ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float)
 ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary)
diff --git a/platformio.ini b/platformio.ini
index c5a0d9a21b..19b850c546 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -4,20 +4,20 @@
 ; It's *not* used during runtime.
 
 [platformio]
-env_default = livingroom8266
+default_envs = livingroom8266
 src_dir = .
 include_dir = include
 
 [common]
 lib_deps =
-    AsyncTCP@1.0.3
-    AsyncMqttClient@0.8.2
+    AsyncTCP@1.1.1
+    AsyncMqttClient-esphome@0.8.3
     ArduinoJson-esphomelib@5.13.3
-    ESP Async WebServer@1.1.1
-    FastLED@3.2.0
-    NeoPixelBus@2.4.1
-    ESPAsyncTCP@1.2.0
-    TinyGPSPlus@1.0.2
+    ESPAsyncWebServer-esphome@1.2.5
+    FastLED@3.2.9
+    NeoPixelBus-esphome@2.5.2
+    ESPAsyncTCP-esphome@1.2.2
+    1655@1.0.2  ; TinyGPSPlus (has name conflict)
 build_flags =
     -Wno-reorder
     -DUSE_WEB_SERVER
@@ -29,15 +29,8 @@ build_flags =
 ; log messages
 src_filter = +<esphome>
 
-[env:livingroom32]
-platform = espressif32@1.6.0
-board = nodemcu-32s
-framework = arduino
-lib_deps = ${common.lib_deps}
-build_flags = ${common.build_flags} -DUSE_ETHERNET
-src_filter = ${common.src_filter} +<tests/livingroom32.cpp>
-
 [env:livingroom8266]
+; use Arduino framework v2.3.0 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760)
 platform = espressif8266@1.8.0
 board = nodemcuv2
 framework = arduino
@@ -47,3 +40,11 @@ lib_deps =
     Hash
 build_flags = ${common.build_flags}
 src_filter = ${common.src_filter} +<tests/livingroom8266.cpp>
+
+[env:livingroom32]
+platform = espressif32@1.11.0
+board = nodemcu-32s
+framework = arduino
+lib_deps = ${common.lib_deps}
+build_flags = ${common.build_flags} -DUSE_ETHERNET
+src_filter = ${common.src_filter} +<tests/livingroom32.cpp>
diff --git a/requirements.txt b/requirements.txt
index 178d3d4459..14ce062000 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,13 @@
-voluptuous>=0.11.5,<0.12
-PyYAML>=5.1,<6
-paho-mqtt>=1.4,<2
-colorlog>=4.0.2
-tornado>=5.1.1,<6
+voluptuous==0.11.7
+PyYAML==5.1.2
+paho-mqtt==1.4.0
+colorlog==4.0.2
+tornado==5.1.1
 typing>=3.6.6;python_version<"3.5"
-protobuf>=3.7,<3.8
-tzlocal>=1.5.1
-pyserial>=3.4,<4
-ifaddr>=0.1.6,<1
-platformio>=3.6.5 ; python_version<"3"
-https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3"
-esptool>=2.6,<3
+protobuf==3.10.0
+tzlocal==2.0.0
+pytz==2019.3
+pyserial==3.4
+ifaddr==0.1.6
+platformio==4.0.3
+esptool==2.7
diff --git a/requirements_test.txt b/requirements_test.txt
index 079b0bf85f..26f14434d8 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,16 +1,17 @@
-voluptuous>=0.11.5,<0.12
-PyYAML>=5.1,<6
-paho-mqtt>=1.4,<2
-colorlog>=4.0.2
-tornado>=5.1.1,<6
-typing>=3.6.6 ; python_version<"3.5"
-protobuf>=3.7,<3.8
-tzlocal>=1.5.1
-pyserial>=3.4,<4
-ifaddr>=0.1.6,<1
-platformio>=3.6.5 ; python_version<"3"
-https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3"
-esptool>=2.6,<3
+voluptuous==0.11.7
+PyYAML==5.1.2
+paho-mqtt==1.4.0
+colorlog==4.0.2
+tornado==5.1.1
+typing>=3.6.6;python_version<"3.5"
+protobuf==3.10.0
+tzlocal==2.0.0
+pytz==2019.3
+pyserial==3.4
+ifaddr==0.1.6
+platformio==4.0.3
+esptool==2.7
+
 pylint==1.9.4 ; python_version<"3"
 pylint==2.3.0 ; python_version>"3"
 flake8==3.6.0
diff --git a/script/.neopixelbus.patch b/script/.neopixelbus.patch
deleted file mode 100644
index 8dc2e74b66..0000000000
--- a/script/.neopixelbus.patch
+++ /dev/null
@@ -1,26 +0,0 @@
---- .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h	2018-12-25 06:37:53.000000000 +0100
-+++ .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h.2	2019-03-01 22:18:10.000000000 +0100
-@@ -169,7 +169,7 @@
-             _i2sBufDesc[indexDesc].sub_sof = 0;
-             _i2sBufDesc[indexDesc].datalen = blockSize;
-             _i2sBufDesc[indexDesc].blocksize = blockSize;
--            _i2sBufDesc[indexDesc].buf_ptr = (uint32_t)is2Buffer;
-+            _i2sBufDesc[indexDesc].buf_ptr = is2Buffer;
-             _i2sBufDesc[indexDesc].unused = 0;
-             _i2sBufDesc[indexDesc].next_link_ptr = (uint32_t)&(_i2sBufDesc[indexDesc + 1]);
-
-@@ -329,11 +329,13 @@
-             case NeoDmaState_Sending:
-                 {
-                     slc_queue_item* finished_item = (slc_queue_item*)SLCRXEDA;
-+                    uint32_t **ptr = reinterpret_cast<uint32_t **>(&finished_item);
-+                    uint32_t dat = *reinterpret_cast<uint32_t *>(ptr);
-
-                     // the data block had actual data sent
-                     // point last state block to first state block thus
-                     // just looping and not sending the data blocks
--                    (finished_item + 1)->next_link_ptr = (uint32_t)(finished_item);
-+                    (finished_item + 1)->next_link_ptr = dat;
-
-                     s_this->_dmaState = NeoDmaState_Idle;
-                 }
diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py
new file mode 100644
index 0000000000..52cbbde678
--- /dev/null
+++ b/script/api_protobuf/api_options_pb2.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: api_options.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()
+
+
+from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='api_options.proto',
+  package='',
+  syntax='proto2',
+  serialized_options=None,
+  serialized_pb=_b('\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto\"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse')
+  ,
+  dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,])
+
+_APISOURCETYPE = _descriptor.EnumDescriptor(
+  name='APISourceType',
+  full_name='APISourceType',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='SOURCE_BOTH', index=0, number=0,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='SOURCE_SERVER', index=1, number=1,
+      serialized_options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='SOURCE_CLIENT', index=2, number=2,
+      serialized_options=None,
+      type=None),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=63,
+  serialized_end=133,
+)
+_sym_db.RegisterEnumDescriptor(_APISOURCETYPE)
+
+APISourceType = enum_type_wrapper.EnumTypeWrapper(_APISOURCETYPE)
+SOURCE_BOTH = 0
+SOURCE_SERVER = 1
+SOURCE_CLIENT = 2
+
+NEEDS_SETUP_CONNECTION_FIELD_NUMBER = 1038
+needs_setup_connection = _descriptor.FieldDescriptor(
+  name='needs_setup_connection', full_name='needs_setup_connection', index=0,
+  number=1038, type=8, cpp_type=7, label=1,
+  has_default_value=True, default_value=True,
+  message_type=None, enum_type=None, containing_type=None,
+  is_extension=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+NEEDS_AUTHENTICATION_FIELD_NUMBER = 1039
+needs_authentication = _descriptor.FieldDescriptor(
+  name='needs_authentication', full_name='needs_authentication', index=1,
+  number=1039, type=8, cpp_type=7, label=1,
+  has_default_value=True, default_value=True,
+  message_type=None, enum_type=None, containing_type=None,
+  is_extension=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+ID_FIELD_NUMBER = 1036
+id = _descriptor.FieldDescriptor(
+  name='id', full_name='id', index=2,
+  number=1036, type=13, cpp_type=3, label=1,
+  has_default_value=True, default_value=0,
+  message_type=None, enum_type=None, containing_type=None,
+  is_extension=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+SOURCE_FIELD_NUMBER = 1037
+source = _descriptor.FieldDescriptor(
+  name='source', full_name='source', index=3,
+  number=1037, type=14, cpp_type=8, label=1,
+  has_default_value=True, default_value=0,
+  message_type=None, enum_type=None, containing_type=None,
+  is_extension=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+IFDEF_FIELD_NUMBER = 1038
+ifdef = _descriptor.FieldDescriptor(
+  name='ifdef', full_name='ifdef', index=4,
+  number=1038, 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=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+LOG_FIELD_NUMBER = 1039
+log = _descriptor.FieldDescriptor(
+  name='log', full_name='log', index=5,
+  number=1039, type=8, cpp_type=7, label=1,
+  has_default_value=True, default_value=True,
+  message_type=None, enum_type=None, containing_type=None,
+  is_extension=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+NO_DELAY_FIELD_NUMBER = 1040
+no_delay = _descriptor.FieldDescriptor(
+  name='no_delay', full_name='no_delay', index=6,
+  number=1040, type=8, cpp_type=7, label=1,
+  has_default_value=True, default_value=False,
+  message_type=None, enum_type=None, containing_type=None,
+  is_extension=True, extension_scope=None,
+  serialized_options=None, file=DESCRIPTOR)
+
+
+_VOID = _descriptor.Descriptor(
+  name='void',
+  full_name='void',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=55,
+  serialized_end=61,
+)
+
+DESCRIPTOR.message_types_by_name['void'] = _VOID
+DESCRIPTOR.enum_types_by_name['APISourceType'] = _APISOURCETYPE
+DESCRIPTOR.extensions_by_name['needs_setup_connection'] = needs_setup_connection
+DESCRIPTOR.extensions_by_name['needs_authentication'] = needs_authentication
+DESCRIPTOR.extensions_by_name['id'] = id
+DESCRIPTOR.extensions_by_name['source'] = source
+DESCRIPTOR.extensions_by_name['ifdef'] = ifdef
+DESCRIPTOR.extensions_by_name['log'] = log
+DESCRIPTOR.extensions_by_name['no_delay'] = no_delay
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+void = _reflection.GeneratedProtocolMessageType('void', (_message.Message,), dict(
+  DESCRIPTOR = _VOID,
+  __module__ = 'api_options_pb2'
+  # @@protoc_insertion_point(class_scope:void)
+  ))
+_sym_db.RegisterMessage(void)
+
+google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_setup_connection)
+google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_authentication)
+google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(id)
+source.enum_type = _APISOURCETYPE
+google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(source)
+google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(ifdef)
+google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(log)
+google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(no_delay)
+
+# @@protoc_insertion_point(module_scope)
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
new file mode 100644
index 0000000000..6357ae38ed
--- /dev/null
+++ b/script/api_protobuf/api_protobuf.py
@@ -0,0 +1,867 @@
+"""Python 3 script to automatically generate C++ classes for ESPHome's native API.
+
+It's pretty crappy spaghetti code, but it works.
+"""
+
+import re
+from pathlib import Path
+from textwrap import dedent
+from subprocess import call
+
+# Generate with
+# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
+import api_options_pb2 as pb
+import google.protobuf.descriptor_pb2 as descriptor
+
+cwd = Path(__file__).parent
+root = cwd.parent.parent / 'esphome' / 'components' / 'api'
+prot = cwd / 'api.protoc'
+call(['protoc', '-o', prot, '-I', root, 'api.proto'])
+content = prot.read_bytes()
+
+d = descriptor.FileDescriptorSet.FromString(content)
+
+
+def indent_list(text, padding=u'  '):
+    return [padding + line for line in text.splitlines()]
+
+
+def indent(text, padding=u'  '):
+    return u'\n'.join(indent_list(text, padding))
+
+
+def camel_to_snake(name):
+    # https://stackoverflow.com/a/1176023
+    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
+    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
+
+
+class TypeInfo():
+    def __init__(self, field):
+        self._field = field
+
+    @property
+    def default_value(self):
+        return ''
+
+    @property
+    def name(self):
+        return self._field.name
+
+    @property
+    def arg_name(self):
+        return self.name
+
+    @property
+    def field_name(self):
+        return self.name
+
+    @property
+    def number(self):
+        return self._field.number
+
+    @property
+    def repeated(self):
+        return self._field.label == 3
+
+    @property
+    def cpp_type(self):
+        raise NotImplementedError
+
+    @property
+    def reference_type(self):
+        return f'{self.cpp_type} '
+
+    @property
+    def const_reference_type(self):
+        return f'{self.cpp_type} '
+
+    @property
+    def public_content(self) -> str:
+        return [self.class_member]
+
+    @property
+    def protected_content(self) -> str:
+        return []
+
+    @property
+    def class_member(self) -> str:
+        return f'{self.cpp_type} {self.field_name}{{{self.default_value}}};  // NOLINT'
+
+    @property
+    def decode_varint_content(self) -> str:
+        content = self.decode_varint
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name} = {content};
+          return true;
+        }}''')
+
+    decode_varint = None
+
+    @property
+    def decode_length_content(self) -> str:
+        content = self.decode_length
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name} = {content};
+          return true;
+        }}''')
+
+    decode_length = None
+
+    @property
+    def decode_32bit_content(self) -> str:
+        content = self.decode_32bit
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name} = {content};
+          return true;
+        }}''')
+
+    decode_32bit = None
+
+    @property
+    def decode_64bit_content(self) -> str:
+        content = self.decode_64bit
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name} = {content};
+          return true;
+        }}''')
+
+    decode_64bit = None
+
+    @property
+    def encode_content(self):
+        return f'buffer.{self.encode_func}({self.number}, this->{self.field_name});'
+
+    encode_func = None
+
+    @property
+    def dump_content(self):
+        o = f'out.append("  {self.name}: ");\n'
+        o += self.dump(f'this->{self.field_name}') + '\n'
+        o += f'out.append("\\n");\n'
+        return o
+
+    dump = None
+
+
+TYPE_INFO = {}
+
+
+def register_type(name):
+    def func(value):
+        TYPE_INFO[name] = value
+        return value
+
+    return func
+
+
+@register_type(1)
+class DoubleType(TypeInfo):
+    cpp_type = 'double'
+    default_value = '0.0'
+    decode_64bit = 'value.as_double()'
+    encode_func = 'encode_double'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%g", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(2)
+class FloatType(TypeInfo):
+    cpp_type = 'float'
+    default_value = '0.0f'
+    decode_32bit = 'value.as_float()'
+    encode_func = 'encode_float'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%g", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(3)
+class Int64Type(TypeInfo):
+    cpp_type = 'int64_t'
+    default_value = '0'
+    decode_varint = 'value.as_int64()'
+    encode_func = 'encode_int64'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%ll", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(4)
+class UInt64Type(TypeInfo):
+    cpp_type = 'uint64_t'
+    default_value = '0'
+    decode_varint = 'value.as_uint64()'
+    encode_func = 'encode_uint64'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%ull", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(5)
+class Int32Type(TypeInfo):
+    cpp_type = 'int32_t'
+    default_value = '0'
+    decode_varint = 'value.as_int32()'
+    encode_func = 'encode_int32'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%d", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(6)
+class Fixed64Type(TypeInfo):
+    cpp_type = 'uint64_t'
+    default_value = '0'
+    decode_64bit = 'value.as_fixed64()'
+    encode_func = 'encode_fixed64'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%ull", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(7)
+class Fixed32Type(TypeInfo):
+    cpp_type = 'uint32_t'
+    default_value = '0'
+    decode_32bit = 'value.as_fixed32()'
+    encode_func = 'encode_fixed32'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%u", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(8)
+class BoolType(TypeInfo):
+    cpp_type = 'bool'
+    default_value = 'false'
+    decode_varint = 'value.as_bool()'
+    encode_func = 'encode_bool'
+
+    def dump(self, name):
+        o = f'out.append(YESNO({name}));'
+        return o
+
+
+@register_type(9)
+class StringType(TypeInfo):
+    cpp_type = 'std::string'
+    default_value = ''
+    reference_type = 'std::string &'
+    const_reference_type = 'const std::string &'
+    decode_length = 'value.as_string()'
+    encode_func = 'encode_string'
+
+    def dump(self, name):
+        o = f'out.append("\'").append({name}).append("\'");'
+        return o
+
+
+@register_type(11)
+class MessageType(TypeInfo):
+    @property
+    def cpp_type(self):
+        return self._field.type_name[1:]
+
+    default_value = ''
+
+    @property
+    def reference_type(self):
+        return f'{self.cpp_type} &'
+
+    @property
+    def const_reference_type(self):
+        return f'const {self.cpp_type} &'
+
+    @property
+    def encode_func(self):
+        return f'encode_message<{self.cpp_type}>'
+
+    @property
+    def decode_length(self):
+        return f'value.as_message<{self.cpp_type}>()'
+
+    def dump(self, name):
+        o = f'{name}.dump_to(out);'
+        return o
+
+
+@register_type(12)
+class BytesType(TypeInfo):
+    cpp_type = 'std::string'
+    default_value = ''
+    reference_type = 'std::string &'
+    const_reference_type = 'const std::string &'
+    decode_length = 'value.as_string()'
+    encode_func = 'encode_string'
+
+    def dump(self, name):
+        o = f'out.append("\'").append({name}).append("\'");'
+        return o
+
+
+@register_type(13)
+class UInt32Type(TypeInfo):
+    cpp_type = 'uint32_t'
+    default_value = '0'
+    decode_varint = 'value.as_uint32()'
+    encode_func = 'encode_uint32'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%u", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(14)
+class EnumType(TypeInfo):
+    @property
+    def cpp_type(self):
+        return "Enum" + self._field.type_name[1:]
+
+    @property
+    def decode_varint(self):
+        return f'value.as_enum<{self.cpp_type}>()'
+
+    default_value = ''
+
+    @property
+    def encode_func(self):
+        return f'encode_enum<{self.cpp_type}>'
+
+    def dump(self, name):
+        o = f'out.append(proto_enum_to_string<{self.cpp_type}>({name}));'
+        return o
+
+
+@register_type(15)
+class SFixed32Type(TypeInfo):
+    cpp_type = 'int32_t'
+    default_value = '0'
+    decode_32bit = 'value.as_sfixed32()'
+    encode_func = 'encode_sfixed32'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%d", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(16)
+class SFixed64Type(TypeInfo):
+    cpp_type = 'int64_t'
+    default_value = '0'
+    decode_64bit = 'value.as_sfixed64()'
+    encode_func = 'encode_sfixed64'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%ll", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(17)
+class SInt32Type(TypeInfo):
+    cpp_type = 'int32_t'
+    default_value = '0'
+    decode_varint = 'value.as_sint32()'
+    encode_func = 'encode_sint32'
+
+    def dump(self, name):
+        o = f'sprintf(buffer, "%d", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+@register_type(18)
+class SInt64Type(TypeInfo):
+    cpp_type = 'int64_t'
+    default_value = '0'
+    decode_varint = 'value.as_sint64()'
+    encode_func = 'encode_sin64'
+
+    def dump(self):
+        o = f'sprintf(buffer, "%ll", {name});\n'
+        o += f'out.append(buffer);'
+        return o
+
+
+class RepeatedTypeInfo(TypeInfo):
+    def __init__(self, field):
+        super(RepeatedTypeInfo, self).__init__(field)
+        self._ti = TYPE_INFO[field.type](field)
+
+    @property
+    def cpp_type(self):
+        return f'std::vector<{self._ti.cpp_type}>'
+
+    @property
+    def reference_type(self):
+        return f'{self.cpp_type} &'
+
+    @property
+    def const_reference_type(self):
+        return f'const {self.cpp_type} &'
+
+    @property
+    def decode_varint_content(self) -> str:
+        content = self._ti.decode_varint
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name}.push_back({content});
+          return true;
+        }}''')
+
+    @property
+    def decode_length_content(self) -> str:
+        content = self._ti.decode_length
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name}.push_back({content});
+          return true;
+        }}''')
+
+    @property
+    def decode_32bit_content(self) -> str:
+        content = self._ti.decode_32bit
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name}.push_back({content});
+          return true;
+        }}''')
+
+    @property
+    def decode_64bit_content(self) -> str:
+        content = self._ti.decode_64bit
+        if content is None:
+            return None
+        return dedent(f'''\
+        case {self.number}: {{
+          this->{self.field_name}.push_back({content});
+          return true;
+        }}''')
+
+    @property
+    def _ti_is_bool(self):
+        # std::vector is specialized for bool, reference does not work
+        return isinstance(self._ti, BoolType)
+
+    @property
+    def encode_content(self):
+        return f"""\
+        for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{
+          buffer.{self._ti.encode_func}({self.number}, it, true);
+        }}"""
+
+    @property
+    def dump_content(self):
+        o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n'
+        o += f'  out.append("  {self.name}: ");\n'
+        o += indent(self._ti.dump('it')) + '\n'
+        o += f'  out.append("\\n");\n'
+        o += f'}}\n'
+        return o
+
+
+def build_enum_type(desc):
+    name = "Enum" + desc.name
+    out = f"enum {name} : uint32_t {{\n"
+    for v in desc.value:
+        out += f'  {v.name} = {v.number},\n'
+    out += '};\n'
+
+    cpp = f"template<>\n"
+    cpp += f"const char *proto_enum_to_string<{name}>({name} value) {{\n"
+    cpp += f"  switch (value) {{\n"
+    for v in desc.value:
+        cpp += f'    case {v.name}: return "{v.name}";\n'
+    cpp += f'    default: return "UNKNOWN";\n'
+    cpp += f'  }}\n'
+    cpp += f'}}\n'
+
+    return out, cpp
+
+
+def build_message_type(desc):
+    public_content = []
+    protected_content = []
+    decode_varint = []
+    decode_length = []
+    decode_32bit = []
+    decode_64bit = []
+    encode = []
+    dump = []
+
+    for field in desc.field:
+        if field.label == 3:
+            ti = RepeatedTypeInfo(field)
+        else:
+            ti = TYPE_INFO[field.type](field)
+        protected_content.extend(ti.protected_content)
+        public_content.extend(ti.public_content)
+        encode.append(ti.encode_content)
+
+        if ti.decode_varint_content:
+            decode_varint.append(ti.decode_varint_content)
+        if ti.decode_length_content:
+            decode_length.append(ti.decode_length_content)
+        if ti.decode_32bit_content:
+            decode_32bit.append(ti.decode_32bit_content)
+        if ti.decode_64bit_content:
+            decode_64bit.append(ti.decode_64bit_content)
+        if ti.dump_content:
+            dump.append(ti.dump_content)
+
+    cpp = ''
+    if decode_varint:
+        decode_varint.append('default:\n  return false;')
+        o = f'bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n'
+        o += '  switch (field_id) {\n'
+        o += indent("\n".join(decode_varint), '    ') + '\n'
+        o += '  }\n'
+        o += '}\n'
+        cpp += o
+        prot = 'bool decode_varint(uint32_t field_id, ProtoVarInt value) override;'
+        protected_content.insert(0, prot)
+    if decode_length:
+        decode_length.append('default:\n  return false;')
+        o = f'bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n'
+        o += '  switch (field_id) {\n'
+        o += indent("\n".join(decode_length), '    ') + '\n'
+        o += '  }\n'
+        o += '}\n'
+        cpp += o
+        prot = 'bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;'
+        protected_content.insert(0, prot)
+    if decode_32bit:
+        decode_32bit.append('default:\n  return false;')
+        o = f'bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n'
+        o += '  switch (field_id) {\n'
+        o += indent("\n".join(decode_32bit), '    ') + '\n'
+        o += '  }\n'
+        o += '}\n'
+        cpp += o
+        prot = 'bool decode_32bit(uint32_t field_id, Proto32Bit value) override;'
+        protected_content.insert(0, prot)
+    if decode_64bit:
+        decode_64bit.append('default:\n  return false;')
+        o = f'bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n'
+        o += '  switch (field_id) {\n'
+        o += indent("\n".join(decode_64bit), '    ') + '\n'
+        o += '  }\n'
+        o += '}\n'
+        cpp += o
+        prot = 'bool decode_64bit(uint32_t field_id, Proto64bit value) override;'
+        protected_content.insert(0, prot)
+
+    o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{\n"
+    o += indent('\n'.join(encode)) + '\n'
+    o += '}\n'
+    cpp += o
+    prot = 'void encode(ProtoWriteBuffer buffer) const override;'
+    public_content.append(prot)
+
+    o = f"void {desc.name}::dump_to(std::string &out) const {{\n"
+    if dump:
+        o += f"  char buffer[64];\n"
+        o += f'  out.append("{desc.name} {{\\n");\n'
+        o += indent('\n'.join(dump)) + '\n'
+        o += f'  out.append("}}");\n'
+    else:
+        o += f'  out.append("{desc.name} {{}}");\n'
+    o += '}\n'
+    cpp += o
+    prot = 'void dump_to(std::string &out) const override;'
+    public_content.append(prot)
+
+    out = f"class {desc.name} : public ProtoMessage {{\n"
+    out += ' public:\n'
+    out += indent('\n'.join(public_content)) + '\n'
+    out += ' protected:\n'
+    out += indent('\n'.join(protected_content)) + '\n'
+    out += "};\n"
+    return out, cpp
+
+
+file = d.file[0]
+content = '''\
+#pragma once
+
+#include "proto.h"
+
+namespace esphome {
+namespace api {
+
+'''
+
+cpp = '''\
+#include "api_pb2.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+'''
+
+for enum in file.enum_type:
+    s, c = build_enum_type(enum)
+    content += s
+    cpp += c
+
+mt = file.message_type
+
+for m in mt:
+    s, c = build_message_type(m)
+    content += s
+    cpp += c
+
+content += '''\
+
+}  // namespace api
+}  // namespace esphome
+'''
+cpp += '''\
+
+}  // namespace api
+}  // namespace esphome
+'''
+
+with open(root / 'api_pb2.h', 'w') as f:
+    f.write(content)
+
+with open(root / 'api_pb2.cpp', 'w') as f:
+    f.write(cpp)
+
+SOURCE_BOTH = 0
+SOURCE_SERVER = 1
+SOURCE_CLIENT = 2
+
+RECEIVE_CASES = {}
+
+class_name = 'APIServerConnectionBase'
+
+ifdefs = {}
+
+
+def get_opt(desc, opt, default=None):
+    if not desc.options.HasExtension(opt):
+        return default
+    return desc.options.Extensions[opt]
+
+
+def build_service_message_type(mt):
+    snake = camel_to_snake(mt.name)
+    id_ = get_opt(mt, pb.id)
+    if id_ is None:
+        return None
+
+    source = get_opt(mt, pb.source, 0)
+
+    ifdef = get_opt(mt, pb.ifdef)
+    log = get_opt(mt, pb.log, True)
+    nodelay = get_opt(mt, pb.no_delay, False)
+    hout = ''
+    cout = ''
+
+    if ifdef is not None:
+        ifdefs[str(mt.name)] = ifdef
+        hout += f'#ifdef {ifdef}\n'
+        cout += f'#ifdef {ifdef}\n'
+
+    if source in (SOURCE_BOTH, SOURCE_SERVER):
+        # Generate send
+        func = f'send_{snake}'
+        hout += f'bool {func}(const {mt.name} &msg);\n'
+        cout += f'bool {class_name}::{func}(const {mt.name} &msg) {{\n'
+        if log:
+            cout += f'  ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
+        cout += f'  this->set_nodelay({str(nodelay).lower()});\n'
+        cout += f'  return this->send_message_<{mt.name}>(msg, {id_});\n'
+        cout += f'}}\n'
+    if source in (SOURCE_BOTH, SOURCE_CLIENT):
+        # Generate receive
+        func = f'on_{snake}'
+        hout += f'virtual void {func}(const {mt.name} &value){{}};\n'
+        case = ''
+        if ifdef is not None:
+            case += f'#ifdef {ifdef}\n'
+        case += f'{mt.name} msg;\n'
+        case += f'msg.decode(msg_data, msg_size);\n'
+        if log:
+            case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
+        case += f'this->{func}(msg);\n'
+        if ifdef is not None:
+            case += f'#endif\n'
+        case += 'break;'
+        RECEIVE_CASES[id_] = case
+
+    if ifdef is not None:
+        hout += f'#endif\n'
+        cout += f'#endif\n'
+
+    return hout, cout
+
+
+hpp = '''\
+#pragma once
+
+#include "api_pb2.h"
+#include "esphome/core/defines.h"
+
+namespace esphome {
+namespace api {
+
+'''
+
+cpp = '''\
+#include "api_pb2_service.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace api {
+
+static const char *TAG = "api.service";
+
+'''
+
+hpp += f'class {class_name} : public ProtoService {{\n'
+hpp += ' public:\n'
+
+for mt in file.message_type:
+    obj = build_service_message_type(mt)
+    if obj is None:
+        continue
+    hout, cout = obj
+    hpp += indent(hout) + '\n'
+    cpp += cout
+
+cases = list(RECEIVE_CASES.items())
+cases.sort()
+hpp += ' protected:\n'
+hpp += f'  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n'
+out = f'bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n'
+out += f'  switch(msg_type) {{\n'
+for i, case in cases:
+    c = f'case {i}: {{\n'
+    c += indent(case) + '\n'
+    c += f'}}'
+    out += indent(c, '    ') + '\n'
+out += '    default: \n'
+out += '      return false;\n'
+out += '  }\n'
+out += '  return true;\n'
+out += '}\n'
+cpp += out
+hpp += '};\n'
+
+serv = file.service[0]
+class_name = 'APIServerConnection'
+hpp += '\n'
+hpp += f'class {class_name} : public {class_name}Base {{\n'
+hpp += ' public:\n'
+hpp_protected = ''
+cpp += '\n'
+
+m = serv.method[0]
+for m in serv.method:
+    func = m.name
+    inp = m.input_type[1:]
+    ret = m.output_type[1:]
+    is_void = ret == 'void'
+    snake = camel_to_snake(inp)
+    on_func = f'on_{snake}'
+    needs_conn = get_opt(m, pb.needs_setup_connection, True)
+    needs_auth = get_opt(m, pb.needs_authentication, True)
+
+    ifdef = ifdefs.get(inp, None)
+
+    if ifdef is not None:
+        hpp += f'#ifdef {ifdef}\n'
+        hpp_protected += f'#ifdef {ifdef}\n'
+        cpp += f'#ifdef {ifdef}\n'
+
+    hpp_protected += f'  void {on_func}(const {inp} &msg) override;\n'
+    hpp += f'  virtual {ret} {func}(const {inp} &msg) = 0;\n'
+    cpp += f'void {class_name}::{on_func}(const {inp} &msg) {{\n'
+    body = ''
+    if needs_conn:
+        body += 'if (!this->is_connection_setup()) {\n'
+        body += '  this->on_no_setup_connection();\n'
+        body += '  return;\n'
+        body += '}\n'
+    if needs_auth:
+        body += 'if (!this->is_authenticated()) {\n'
+        body += '  this->on_unauthenticated_access();\n'
+        body += '  return;\n'
+        body += '}\n'
+
+    if is_void:
+        body += f'this->{func}(msg);\n'
+    else:
+        body += f'{ret} ret = this->{func}(msg);\n'
+        ret_snake = camel_to_snake(ret)
+        body += f'if (!this->send_{ret_snake}(ret)) {{\n'
+        body += f'  this->on_fatal_error();\n'
+        body += '}\n'
+    cpp += indent(body) + '\n' + '}\n'
+
+    if ifdef is not None:
+        hpp += f'#endif\n'
+        hpp_protected += f'#endif\n'
+        cpp += f'#endif\n'
+
+hpp += ' protected:\n'
+hpp += hpp_protected
+hpp += '};\n'
+
+hpp += '''\
+
+}  // namespace api
+}  // namespace esphome
+'''
+cpp += '''\
+
+}  // namespace api
+}  // namespace esphome
+'''
+
+with open(root / 'api_pb2_service.h', 'w') as f:
+    f.write(hpp)
+
+with open(root / 'api_pb2_service.cpp', 'w') as f:
+    f.write(cpp)
+
+prot.unlink()
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 27b45dbe27..922d94f2aa 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -7,6 +7,7 @@ import fnmatch
 import os.path
 import subprocess
 import sys
+import re
 
 
 def find_all(a_str, sub):
@@ -28,15 +29,18 @@ EXECUTABLE_BIT = {
     s[3].strip(): int(s[0]) for s in lines
 }
 files = [s[3].strip() for s in lines]
+files = list(filter(os.path.exists, files))
 files.sort()
 
-file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico',
-              '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg')
+file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg',
+              '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg',
+              '.woff', '.woff2', '')
 cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc')
-ignore_types = ('.ico',)
+ignore_types = ('.ico', '.woff', '.woff2', '')
 
 LINT_FILE_CHECKS = []
 LINT_CONTENT_CHECKS = []
+LINT_POST_CHECKS = []
 
 
 def run_check(lint_obj, fname, *args):
@@ -82,6 +86,31 @@ def lint_content_check(**kwargs):
     return decorator
 
 
+def lint_post_check(func):
+    _add_check(LINT_POST_CHECKS, func)
+    return func
+
+
+def lint_re_check(regex, **kwargs):
+    prog = re.compile(regex, re.MULTILINE)
+    decor = lint_content_check(**kwargs)
+
+    def decorator(func):
+        def new_func(fname, content):
+            errors = []
+            for match in prog.finditer(content):
+                if 'NOLINT' in match.group(0):
+                    continue
+                lineno = content.count("\n", 0, match.start()) + 1
+                err = func(fname, match)
+                if err is None:
+                    continue
+                errors.append("{} See line {}.".format(err, lineno))
+            return errors
+        return decor(new_func)
+    return decorator
+
+
 def lint_content_find_check(find, **kwargs):
     decor = lint_content_check(**kwargs)
 
@@ -90,9 +119,12 @@ def lint_content_find_check(find, **kwargs):
             find_ = find
             if callable(find):
                 find_ = find(fname, content)
+            errors = []
             for line, col in find_all(content, find_):
                 err = func(fname)
-                return "{err} See line {line}:{col}.".format(err=err, line=line+1, col=col+1)
+                errors.append("{err} See line {line}:{col}."
+                              "".format(err=err, line=line+1, col=col+1))
+            return errors
         return decor(new_func)
     return decorator
 
@@ -104,7 +136,7 @@ def lint_ino(fname):
 
 @lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [
     '.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc',
-    'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*'
+    'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*',
 ])
 def lint_ext_check(fname):
     return "This file extension is not a registered file type. If this is an error, please " \
@@ -124,7 +156,6 @@ def lint_executable_bit(fname):
 
 @lint_content_find_check('\t', exclude=[
     'esphome/dashboard/static/ace.js', 'esphome/dashboard/static/ext-searchbox.js',
-    'script/.neopixelbus.patch',
 ])
 def lint_tabs(fname):
     return "File contains tab character. Please convert tabs to spaces."
@@ -135,13 +166,105 @@ def lint_newline(fname):
     return "File contains windows newline. Please set your editor to unix newline mode."
 
 
-@lint_content_check()
+@lint_content_check(exclude=['*.svg'])
 def lint_end_newline(fname, content):
     if content and not content.endswith('\n'):
         return "File does not end with a newline, please add an empty line at the end of the file."
     return None
 
 
+CPP_RE_EOL = r'\s*?(?://.*?)?$'
+
+
+def highlight(s):
+    return '\033[36m{}\033[0m'.format(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'])
+def lint_no_defines(fname, match):
+    s = highlight('static const uint8_t {} = {};'.format(match.group(1), match.group(2)))
+    return ("#define macros for integer constants are not allowed, please use "
+            "{} style instead (replace uint8_t with the appropriate "
+            "datatype). See also Google style guide.".format(s))
+
+
+@lint_re_check(r'^\s*delay\((\d+)\);' + CPP_RE_EOL, include=cpp_include)
+def lint_no_long_delays(fname, match):
+    duration_ms = int(match.group(1))
+    if duration_ms < 50:
+        return None
+    return (
+        "{} - long calls to delay() are not allowed in ESPHome because everything executes "
+        "in one thread. Calling delay() will block the main thread and slow down ESPHome.\n"
+        "If there's no way to work around the delay() and it doesn't execute often, please add "
+        "a '// NOLINT' comment to the line."
+        "".format(highlight(match.group(0).strip()))
+    )
+
+
+@lint_content_check(include=['esphome/const.py'])
+def lint_const_ordered(fname, content):
+    lines = content.splitlines()
+    errors = []
+    for start in ['CONF_', 'ICON_', 'UNIT_']:
+        matching = [(i+1, line) for i, line in enumerate(lines) if line.startswith(start)]
+        ordered = list(sorted(matching, key=lambda x: x[1].replace('_', ' ')))
+        ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)]
+        for (mi, ml), (oi, ol) in zip(matching, ordered):
+            if ml == ol:
+                continue
+            target = next(i for i, l in ordered if l == ml)
+            target_text = next(l for i, l in matching if target == i)
+            errors.append("Constant {} is not ordered, please make sure all constants are ordered. "
+                          "See line {} (should go to line {}, {})"
+                          "".format(highlight(ml), mi, target, target_text))
+    return errors
+
+
+@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=['*.py'])
+def lint_conf_matches(fname, match):
+    const = match.group(1)
+    value = match.group(2)
+    const_norm = const.lower()
+    value_norm = value.replace('.', '_')
+    if const_norm == value_norm:
+        return None
+    return ("Constant {} does not match value {}! Please make sure the constant's name matches its "
+            "value!"
+            "".format(highlight('CONF_' + const), highlight(value)))
+
+
+CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$'
+with codecs.open('esphome/const.py', 'r', encoding='utf-8') as f_handle:
+    constants_content = f_handle.read()
+CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)]
+
+CONSTANTS_USES = collections.defaultdict(list)
+
+
+@lint_re_check(CONF_RE, include=['*.py'], exclude=['esphome/const.py'])
+def lint_conf_from_const_py(fname, match):
+    name = match.group(1)
+    if name not in CONSTANTS:
+        CONSTANTS_USES[name].append(fname)
+        return None
+    return ("Constant {} has already been defined in const.py - please import the constant from "
+            "const.py directly.".format(highlight(name)))
+
+
+@lint_post_check
+def lint_constants_usage():
+    errors = []
+    for constant, uses in CONSTANTS_USES.items():
+        if len(uses) < 4:
+            continue
+        errors.append("Constant {} is defined in {} files. Please move all definitions of the "
+                      "constant to const.py (Uses: {})"
+                      "".format(highlight(constant), len(uses), ', '.join(uses)))
+    return errors
+
+
 def relative_cpp_search_text(fname, content):
     parts = fname.split('/')
     integration = parts[2]
@@ -164,7 +287,8 @@ def relative_py_search_text(fname, content):
     return 'esphome.components.{}'.format(integration)
 
 
-@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'])
+@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'],
+                         exclude=['esphome/components/web_server/__init__.py'])
 def lint_relative_py_import(fname):
     return ("Component contains absolute import - Components must always use "
             "relative imports within the integration.\n"
@@ -174,6 +298,19 @@ def lint_relative_py_import(fname):
             '  from . import abc_ns\n\n')
 
 
+@lint_content_check(include=['esphome/components/*.h', 'esphome/components/*.cpp',
+                             'esphome/components/*.tcc'])
+def lint_namespace(fname, content):
+    expected_name = re.match(r'^esphome/components/([^/]+)/.*',
+                             fname.replace(os.path.sep, '/')).group(1)
+    search = 'namespace {}'.format(expected_name)
+    if search in content:
+        return None
+    return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \
+           'functions in a separate namespace that matches the integration\'s name. ' \
+           'Please make sure the file contains {}'.format(highlight(search))
+
+
 @lint_content_find_check('"esphome.h"', include=cpp_include, exclude=['tests/custom.h'])
 def lint_esphome_h(fname):
     return ("File contains reference to 'esphome.h' - This file is "
@@ -201,6 +338,7 @@ def lint_pragma_once(fname, content):
     'esphome/components/stepper/stepper.h',
     'esphome/components/switch/switch.h',
     'esphome/components/text_sensor/text_sensor.h',
+    'esphome/components/climate/climate.h',
     'esphome/core/component.h',
     'esphome/core/esphal.h',
     'esphome/core/log.h',
@@ -229,7 +367,7 @@ def add_errors(fname, errs):
 for fname in files:
     _, ext = os.path.splitext(fname)
     run_checks(LINT_FILE_CHECKS, fname, fname)
-    if ext in ('.ico',):
+    if ext in ignore_types:
         continue
     try:
         with codecs.open(fname, 'r', encoding='utf-8') as f_handle:
@@ -239,6 +377,8 @@ for fname in files:
         continue
     run_checks(LINT_CONTENT_CHECKS, fname, fname, content)
 
+run_checks(LINT_POST_CHECKS, 'POST')
+
 for f, errs in sorted(errors.items()):
     print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f))
     for err in errs:
diff --git a/script/clang-tidy b/script/clang-tidy
index 39df87df22..f178e036b1 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -54,7 +54,6 @@ def run_tidy(args, tmpdir, queue, lock, failed_files):
             if rc != 0:
                 print()
                 print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path))
-                print(invocation_s)
                 print(output)
                 print()
                 failed_files.append(path)
diff --git a/script/lint-cpp b/script/lint-cpp
index 88728c9abd..170d61d539 100755
--- a/script/lint-cpp
+++ b/script/lint-cpp
@@ -6,9 +6,6 @@ cd "$(dirname "$0")/.."
 if [[ ! -e ".gcc-flags.json" ]]; then
   pio init --ide atom
 fi
-if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then
-  patch -p0 < script/.neopixelbus.patch
-fi
 
 set -x
 
diff --git a/script/quicklint b/script/quicklint
index 06c31d519d..e391ca3276 100755
--- a/script/quicklint
+++ b/script/quicklint
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/bin/bash
 
 set -e
 
diff --git a/script/setup b/script/setup
index a65eb23a63..b6cff39f0c 100755
--- a/script/setup
+++ b/script/setup
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/bin/bash
 # Set up ESPHome dev environment
 
 set -e
diff --git a/setup.py b/setup.py
index a93cd0b325..53acef5a30 100755
--- a/setup.py
+++ b/setup.py
@@ -23,16 +23,17 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
 DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__)
 
 REQUIRES = [
-    'voluptuous>=0.11.5,<0.12',
-    'PyYAML>=5.1,<6',
-    'paho-mqtt>=1.4,<2',
-    'colorlog>=4.0.2',
-    'tornado>=5.1.1,<6',
+    'voluptuous==0.11.7',
+    'PyYAML==5.1.2',
+    'paho-mqtt==1.4.0',
+    'colorlog==4.0.2',
+    'tornado==5.1.1',
     'typing>=3.6.6;python_version<"3.5"',
-    'protobuf>=3.7,<3.8',
-    'tzlocal>=1.5.1',
-    'pyserial>=3.4,<4',
-    'ifaddr>=0.1.6,<1',
+    'protobuf==3.10.0',
+    'tzlocal==2.0.0',
+    'pytz==2019.3',
+    'pyserial==3.4',
+    'ifaddr==0.1.6',
 ]
 
 # If you have problems importing platformio and esptool as modules you can set
@@ -40,8 +41,8 @@ REQUIRES = [
 # This means they have to be in your $PATH.
 if os.environ.get('ESPHOME_USE_SUBPROCESS') is None:
     REQUIRES.extend([
-        'platformio>=3.6.5',
-        'esptool>=2.6,<3',
+        'platformio==4.0.3',
+        'esptool==2.7',
     ])
 
 CLASSIFIERS = [
diff --git a/tests/custom.h b/tests/custom.h
index b392819efb..278e300785 100644
--- a/tests/custom.h
+++ b/tests/custom.h
@@ -3,7 +3,12 @@
 
 class CustomSensor : public Component, public Sensor {
  public:
-  void loop() override { publish_state(42.0); }
+  void loop() override {
+    // Test id() helper
+    float value = id(my_sensor).state;
+    id(my_global_string) = "Hello World";
+    publish_state(42.0);
+  }
 };
 
 class CustomTextSensor : public Component, public TextSensor {
@@ -36,3 +41,32 @@ class CustomFloatOutput : public FloatOutput, public Component {
  protected:
   void write_state(float state) override { ESP_LOGD("custom_output", "Setting %f", state); }
 };
+
+class CustomNativeAPI : public Component, public CustomAPIDevice {
+ public:
+  void setup() override {
+    register_service(&CustomNativeAPI::on_hello_world, "hello_world");
+    register_service(&CustomNativeAPI::on_start_dryer, "start_dryer", {"value"});
+    register_service(&CustomNativeAPI::on_many_values, "many_values", {"bool", "int", "float", "str1", "str2"});
+    subscribe_homeassistant_state(&CustomNativeAPI::on_light_state, "light.my_light");
+  }
+
+  void on_hello_world() { ESP_LOGD("custom_api", "Hello World from native API service!"); }
+  void on_start_dryer(int value) { ESP_LOGD("custom_api", "Value is %d", value); }
+  void on_many_values(bool a_bool, int a_int, float a_float, std::string str1, std::string str2) {
+    ESP_LOGD("custom_api", "Bool: %s", ONOFF(a_bool));
+    ESP_LOGD("custom_api", "Int: %d", a_int);
+    ESP_LOGD("custom_api", "Float: %f", a_float);
+    ESP_LOGD("custom_api", "String1: %s", str1.c_str());
+    ESP_LOGD("custom_api", "String2: %s", str2.c_str());
+    ESP_LOGD("custom_api", "Connected: %s", YESNO(is_connected()));
+
+    call_homeassistant_service("light.turn_on", {
+                                                    {"entity_id", "light.my_light"},
+                                                    {"brightness", "127"},
+                                                });
+    // no args
+    call_homeassistant_service("homeassistant.restart");
+  }
+  void on_light_state(std::string state) { ESP_LOGD("custom_api", "Got state %s", state.c_str()); }
+};
diff --git a/tests/test1.yaml b/tests/test1.yaml
index fac39b6a06..66f0220836 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -73,10 +73,10 @@ mqtt:
             ESP_LOGD("main", "Got message %s", x.c_str());
     - topic: livingroom/ota_mode
       then:
-      - deep_sleep.prevent: deep_sleep_1
+      - deep_sleep.prevent
     - topic: livingroom/ota_mode
       then:
-      - deep_sleep.enter: deep_sleep_1
+      - deep_sleep.enter:
   on_json_message:
     topic: the/topic
     then:
@@ -163,7 +163,6 @@ deep_sleep:
   sleep_duration: 50s
   wakeup_pin: GPIO39
   wakeup_pin_mode: INVERT_WAKEUP
-  id: deep_sleep_1
 
 ads1115:
   address: 0x48
@@ -171,6 +170,10 @@ ads1115:
 dallas:
   pin: GPIO23
 
+as3935_spi:
+  cs_pin: GPIO12
+  irq_pin: GPIO13
+
 sensor:
   - platform: adc
     pin: A0
@@ -182,6 +185,7 @@ sensor:
     accuracy_decimals: 5
     expire_after: 120s
     setup_priority: -100
+    force_update: true
     filters:
       - offset: 2.0
       - multiply: 1.2
@@ -191,6 +195,10 @@ sensor:
         - 100.0 -> 102.5
       - filter_out: 42.0
       - filter_out: nan
+      - median:
+          window_size: 5
+          send_every: 5
+          send_first_at: 3
       - sliding_window_moving_average:
           window_size: 15
           send_every: 15
@@ -244,6 +252,35 @@ sensor:
     state_topic: hi/me
     retain: false
     availability:
+  - platform: atm90e32
+    cs_pin: 5
+    phase_a:
+      voltage:
+        name: "EMON Line Voltage A"
+      current:
+        name: "EMON CT1 Current"
+      power:
+        name: "EMON Active Power CT1"
+      gain_voltage: 47660
+      gain_ct: 12577
+    phase_b:
+      current:
+        name: "EMON CT2 Current"
+      power:
+        name: "EMON Active Power CT2"
+      gain_voltage: 47660
+      gain_ct: 12577
+    phase_c:
+      current:
+        name: "EMON CT3 Current"
+      power:
+        name: "EMON Active Power CT3"
+      gain_voltage: 47660
+      gain_ct: 12577
+    frequency:
+      name: "EMON Line Frequency"
+    line_frequency: 50Hz
+    gain_pga: 2X
   - platform: bh1750
     name: "Living Room Brightness 3"
     internal: true
@@ -347,6 +384,7 @@ sensor:
     current_resistor: 0.001 ohm
     voltage_divider: 2351
     change_mode_every: 16
+    initial_mode: VOLTAGE
   - platform: total_daily_energy
     power_id: hlw8012_power
     name: "HLW8012 Total Daily Energy"
@@ -419,6 +457,7 @@ sensor:
     temperature:
       name: "MH-Z19 Temperature"
     update_interval: 15s
+    automatic_baseline_calibration: false
   - platform: mpu6050
     address: 0x68
     accel_x:
@@ -452,6 +491,7 @@ sensor:
     update_interval: 15s
   - platform: rotary_encoder
     name: "Rotary Encoder"
+    id: rotary_encoder1
     pin_a: GPIO23
     pin_b: GPIO24
     pin_reset: GPIO25
@@ -462,9 +502,20 @@ sensor:
     resolution: 4
     min_value: -10
     max_value: 30
+    on_value:
+      - sensor.rotary_encoder.set_value:
+          id: rotary_encoder1
+          value: 10
+      - sensor.rotary_encoder.set_value:
+          id: rotary_encoder1
+          value: !lambda 'return -1;'
   - platform: pulse_width
     name: Pulse Width
     pin: GPIO12
+  - platform: senseair
+    co2:
+      name: "SenseAir CO2 Value"
+    update_interval: 15s
   - platform: sht3xd
     temperature:
       name: "Living Room Temperature 8"
@@ -472,6 +523,34 @@ sensor:
       name: "Living Room Humidity 8"
     address: 0x44
     update_interval: 15s
+  - platform: sts3x
+    name: "Living Room Temperature 9"
+    address: 0x4A
+  - platform: scd30
+    co2:
+      name: "Living Room CO2 9"
+    temperature:
+      name: "Living Room Temperature 9"
+    humidity:
+      name: "Living Room Humidity 9"
+    address: 0x61
+    update_interval: 15s
+  - platform: sgp30
+    eco2:
+      name: "Workshop eCO2"
+      accuracy_decimals: 1
+    tvoc:
+      name: "Workshop TVOC"
+      accuracy_decimals: 1
+    address: 0x58
+    update_interval: 5s
+  - platform: shtcx
+    temperature:
+      name: "Living Room Temperature 10"
+    humidity:
+      name: "Living Room Humidity 10"
+    address: 0x70
+    update_interval: 15s
   - platform: template
     name: "Template Sensor"
     id: template_sensor
@@ -534,7 +613,28 @@ sensor:
       name: CCS811 TVOC
     update_interval: 30s
     baseline: 0x4242
-
+  - platform: tx20
+    wind_speed:
+      name: "Windspeed"
+    wind_direction_degrees:
+      name: "Winddirection Degrees"
+    pin: 
+      number: GPIO04
+      mode: INPUT
+  - platform: zyaura
+    clock_pin: GPIO5
+    data_pin: GPIO4
+    co2:
+      name: "ZyAura CO2"
+    temperature:
+      name: "ZyAura Temperature"
+    humidity:
+      name: "ZyAura Humidity"
+  - platform: as3935
+    lightning_energy:
+      name: "Lightning Energy"
+    distance:
+      name: "Distance Storm"
 
 esp32_touch:
   setup_mode: False
@@ -617,6 +717,7 @@ binary_sensor:
     name: "ESP32 Touch Pad GPIO27"
     pin: GPIO27
     threshold: 1000
+    id: btn_left
   - platform: nextion
     page_id: 0
     component_id: 2
@@ -641,6 +742,12 @@ binary_sensor:
       - binary_sensor.template.publish:
           id: garage_door
           state: OFF
+      - output.ledc.set_frequency:
+          id: gpio_19
+          frequency: 500.0Hz
+      - output.ledc.set_frequency:
+          id: gpio_19
+          frequency: !lambda 'return 500.0;'
   - platform: pn532
     uid: 74-10-37-94
     name: "PN532 NFC Tag"
@@ -655,12 +762,20 @@ binary_sensor:
       mode: INPUT
       inverted: True
   - platform: gpio
-    name: "MCP binary sensor"
+    name: "MCP21 binary sensor"
     pin:
       mcp23017: mcp23017_hub
       number: 1
       mode: INPUT
       inverted: True
+  - platform: gpio
+    name: "MCP22 binary sensor"
+    pin:
+      mcp23008: mcp23008_hub
+      number: 7
+      mode: INPUT_PULLUP
+      inverted: False
+
   - platform: remote_receiver
     name: "Raw Remote Receiver Test"
     raw:
@@ -668,11 +783,21 @@ binary_sensor:
              3700, -2263, 1712, -4254, 1711, -4249, 1715, -2266, 1710, -2267,
              1709, -2265, 3704, -4250, 1712, -4254, 3700, -2260, 1714, -2265,
              1712, -2262, 1714, -2267, 1709]
+  - platform: as3935
+    name: "Storm Alert"
 
 pca9685:
   frequency: 500
   address: 0x0
 
+tlc59208f:
+  - address: 0x20
+    id: tlc59208f_1
+  - address: 0x22
+    id: tlc59208f_2
+  - address: 0x24
+    id: tlc59208f_3
+
 my9231:
   data_pin: GPIO12
   clock_pin: GPIO14
@@ -690,7 +815,6 @@ output:
     pin: 19
     id: gpio_19
     frequency: 1500Hz
-    bit_depth: 8
     channel: 14
     max_power: 0.5
   - platform: pca9685
@@ -717,6 +841,42 @@ output:
   - platform: pca9685
     id: pca_7
     channel: 7
+  - platform: tlc59208f
+    id: tlc_0
+    channel: 0
+    tlc59208f_id: 'tlc59208f_1'
+  - platform: tlc59208f
+    id: tlc_1
+    channel: 1
+    tlc59208f_id: 'tlc59208f_1'
+  - platform: tlc59208f
+    id: tlc_2
+    channel: 2
+    tlc59208f_id: 'tlc59208f_1'
+  - platform: tlc59208f
+    id: tlc_3
+    channel: 0
+    tlc59208f_id: 'tlc59208f_2'
+  - platform: tlc59208f
+    id: tlc_4
+    channel: 1
+    tlc59208f_id: 'tlc59208f_2'
+  - platform: tlc59208f
+    id: tlc_5
+    channel: 2
+    tlc59208f_id: 'tlc59208f_2'
+  - platform: tlc59208f
+    id: tlc_6
+    channel: 0
+    tlc59208f_id: 'tlc59208f_3'
+  - platform: tlc59208f
+    id: tlc_7
+    channel: 1
+    tlc59208f_id: 'tlc59208f_3'
+  - platform: tlc59208f
+    id: tlc_8
+    channel: 2
+    tlc59208f_id: 'tlc59208f_3'
   - platform: gpio
     id: id2
     pin:
@@ -731,6 +891,13 @@ output:
       number: 0
       mode: OUTPUT
       inverted: False
+  - platform: gpio
+    id: id23
+    pin:
+      mcp23008: mcp23008_hub
+      number: 0
+      mode: OUTPUT
+      inverted: False
   - platform: my9231
     id: my_0
     channel: 0
@@ -862,6 +1029,11 @@ light:
         name: Flicker Effect With Custom Values
         update_interval: 16ms
         intensity: 5%
+    - addressable_lambda:
+          name: "Test For Custom Lambda Effect"
+          lambda: |-
+            it[0] = current_color;
+                
     - automation:
         name: Custom Effect
         sequence:
@@ -982,7 +1154,7 @@ switch:
     name: RC Switch Raw
     turn_on_action:
       remote_transmitter.transmit_rc_switch_raw:
-        code: '001010011001111101011011'
+        code: '00101001100111110101xxxx'
         protocol: 1
   - platform: template
     name: RC Switch Type A
@@ -1124,6 +1296,16 @@ interval:
         if (true) return id(page1); else return id(page2);
     -  display.page.show_next: display1
     -  display.page.show_previous: display1
+  - interval: 2s
+    then:
+      - lambda: |-
+            static uint16_t btn_left_state = id(btn_left)->get_value();
+
+            ESP_LOGD("adaptive touch", "___  Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold());
+            
+            btn_left_state = ((uint32_t) id(btn_left)->get_value()  +  63 * (uint32_t)btn_left_state) >> 6;
+
+            id(btn_left)->set_threshold(btn_left_state * 0.9);
 
 display:
 - platform: lcd_gpio
@@ -1152,10 +1334,11 @@ display:
     it.set_component_value("gauge", 50);
     it.set_component_text("textview", "Hello World!");
 - platform: ssd1306_i2c
-  model: "SSD1306 128x64"
+  model: "SSD1306_128X64"
   reset_pin: GPIO23
   address: 0x3C
   id: display1
+  brightness: 60%
   pages:
     - id: page1
       lambda: |-
@@ -1170,6 +1353,13 @@ display:
   reset_pin: GPIO23
   lambda: |-
     it.rectangle(0, 0, it.get_width(), it.get_height());
+- platform: ssd1325_spi
+  model: "SSD1325 128x64"
+  cs_pin: GPIO23
+  dc_pin: GPIO23
+  reset_pin: GPIO23
+  lambda: |-
+    it.rectangle(0, 0, it.get_width(), it.get_height());
 - platform: waveshare_epaper
   cs_pin: GPIO23
   dc_pin: GPIO23
@@ -1241,6 +1431,10 @@ pcf8574:
 mcp23017:
   - id: 'mcp23017_hub'
 
+mcp23008:
+  - id: 'mcp23008_hub'
+    address: 0x22
+
 stepper:
 - platform: a4988
   id: my_stepper
diff --git a/tests/test2.yaml b/tests/test2.yaml
index e3a9b0da85..097c29285a 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -45,35 +45,56 @@ logger:
   level: DEBUG
 
 web_server:
+  auth:
+    username: admin
+    password: admin
 
 deep_sleep:
   run_duration: 20s
   sleep_duration: 50s
 
+as3935_i2c:
+  irq_pin: GPIO12
+
+
 sensor:
   - platform: ble_rssi
     mac_address: AC:37:43:77:5F:4C
     name: "BLE Google Home Mini RSSI value"
-  - platform: xiaomi_miflora
+  - platform: xiaomi_hhccjcy01
     mac_address: 94:2B:FF:5C:91:61
     temperature:
-      name: "Xiaomi MiFlora Temperature"
+      name: "Xiaomi HHCCJCY01 Temperature"
     moisture:
-      name: "Xiaomi MiFlora Moisture"
+      name: "Xiaomi HHCCJCY01 Moisture"
     illuminance:
-      name: "Xiaomi MiFlora Illuminance"
+      name: "Xiaomi HHCCJCY01 Illuminance"
     conductivity:
-      name: "Xiaomi MiFlora Soil Conductivity"
+      name: "Xiaomi HHCCJCY01 Soil Conductivity"
     battery_level:
-      name: "Xiaomi MiFlora Battery Level"
-  - platform: xiaomi_mijia
+      name: "Xiaomi HHCCJCY01 Battery Level"
+  - platform: xiaomi_lywsdcgq
     mac_address: 7A:80:8E:19:36:BA
     temperature:
-      name: "Xiaomi MiJia Temperature"
+      name: "Xiaomi LYWSDCGQ Temperature"
     humidity:
-      name: "Xiaomi MiJia Humidity"
+      name: "Xiaomi LYWSDCGQ Humidity"
     battery_level:
-      name: "Xiaomi MiJia Battery Level"
+      name: "Xiaomi LYWSDCGQ Battery Level"
+  - platform: xiaomi_lywsd02
+    mac_address: 3F:5B:7D:82:58:4E
+    temperature:
+      name: "Xiaomi LYWSD02 Temperature"
+    humidity:
+      name: "Xiaomi LYWSD02 Humidity"    
+  - platform: xiaomi_cgg1
+    mac_address: 7A:80:8E:19:36:BA
+    temperature:
+      name: "Xiaomi CGG1 Temperature"
+    humidity:
+      name: "Xiaomi CGG1 Humidity"
+    battery_level:
+      name: "Xiaomi CGG1 Battery Level"
   - platform: pmsx003
     type: PMSX003
     pm_1_0:
@@ -125,6 +146,11 @@ sensor:
   - platform: homeassistant
     entity_id: sensor.hello_world
     id: ha_hello_world
+  - platform: as3935
+    lightning_energy:
+      name: "Lightning Energy"
+    distance:
+      name: "Distance Storm"
 
 time:
 - platform: homeassistant
@@ -163,13 +189,14 @@ binary_sensor:
   - platform: homeassistant
     entity_id: binary_sensor.hello_world
     id: ha_hello_world_binary
+  - platform: as3935
+    name: "Storm Alert"
 
 remote_receiver:
   pin: GPIO32
   dump: []
 
 esp32_ble_tracker:
-  scan_interval: 300s
 
 #esp32_ble_beacon:
 #  type: iBeacon
diff --git a/tests/test3.yaml b/tests/test3.yaml
index 6b64af8981..d1596c8f41 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -1,5 +1,6 @@
 esphome:
   name: $devicename
+  comment: $devicecomment
   platform: ESP8266
   board: d1_mini
   build_path: build/test3
@@ -12,6 +13,7 @@ esphome:
 
 substitutions:
   devicename: test3
+  devicecomment: test3 device
 
 api:
   port: 8000
@@ -40,6 +42,101 @@ api:
         - stepper.set_target:
             id: my_stepper2
             target: !lambda 'return int_;'
+    - service: array_types
+      variables:
+        bool_arr: bool[]
+        int_arr: int[]
+        float_arr: float[]
+        string_arr: string[]
+      then:
+        - logger.log:
+            format: 'Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)'
+            args:
+              - YESNO(bool_arr[0])
+              - bool_arr.size()
+              - int_arr[0]
+              - int_arr.size()
+              - float_arr[0]
+              - float_arr.size()
+              - string_arr[0].c_str()
+              - string_arr.size()
+    - service: dfplayer_next
+      then:
+        - dfplayer.play_next:
+    - service: dfplayer_previous
+      then:
+        - dfplayer.play_previous:
+    - service: dfplayer_play
+      variables:
+        file: int
+      then:
+        - dfplayer.play: !lambda 'return file;'
+    - service: dfplayer_play_loop
+      variables:
+        file: int
+        loop_: bool
+      then:
+        - dfplayer.play:
+            file: !lambda 'return file;'
+            loop: !lambda 'return loop_;'
+    - service: dfplayer_play_folder
+      variables:
+        folder: int
+        file: int
+      then:
+        - dfplayer.play_folder:
+            folder: !lambda 'return folder;'
+            file: !lambda 'return file;'
+
+    - service: dfplayer_play_loo_folder
+      variables:
+        folder: int
+      then:
+        - dfplayer.play_folder:
+            folder: !lambda 'return folder;'
+            loop: True
+
+    - service: dfplayer_set_device
+      variables:
+        device: int
+      then:
+        - dfplayer.set_device:
+            device: TF_CARD
+
+    - service: dfplayer_set_volume
+      variables:
+        volume: int
+      then:
+        - dfplayer.set_volume: !lambda 'return volume;'
+    - service: dfplayer_set_eq
+      variables:
+        preset: int
+      then:
+        - dfplayer.set_eq: !lambda 'return static_cast<dfplayer::EqPreset>(preset);'
+
+    - service: dfplayer_sleep
+      then:
+        - dfplayer.sleep
+
+    - service: dfplayer_reset
+      then:
+        - dfplayer.reset
+
+    - service: dfplayer_start
+      then:
+        - dfplayer.start
+
+    - service: dfplayer_pause
+      then:
+        - dfplayer.pause
+
+    - service: dfplayer_stop
+      then:
+        - dfplayer.stop
+
+    - service: dfplayer_random
+      then:
+        - dfplayer.random
 
 wifi:
   ssid: 'MySSID'
@@ -115,6 +212,14 @@ sensor:
       - calibrate_linear:
           - 0 -> 0
           - 100 -> 100
+      - calibrate_polynomial:
+          degree: 3
+          datapoints:
+            - 0 -> 0
+            - 100 -> 200
+            - 400 -> 500
+            - -50 -> -1000
+            - -100 -> -10000
   - platform: resistance
     sensor: my_sensor
     configuration: DOWNSTREAM
@@ -175,6 +280,42 @@ sensor:
         value: 15.0
       - binary_sensor: bin3
         value: 100.0
+  - platform: ade7953
+    voltage:
+      name: ADE7953 Voltage
+    current_a:
+      name: ADE7953 Current A
+    current_b:
+      name: ADE7953 Current B
+    active_power_a:
+      name: ADE7953 Active Power A
+    active_power_b:
+      name: ADE7953 Active Power B
+  - platform: pzem004t
+    voltage:
+      name: "PZEM00T Voltage"
+    current:
+      name: "PZEM004T Current"
+    power:
+      name: "PZEM004T Power"
+  - platform: pzemac
+    voltage:
+      name: "PZEMAC Voltage"
+    current:
+      name: "PZEMAC Current"
+    power:
+      name: "PZEMAC Power"
+    frequency:
+      name: "PZEMAC Frequency"
+    power_factor:
+      name: "PZEMAC Power Factor"
+  - platform: pzemdc
+    voltage:
+      name: "PZEMDC Voltage"
+    current:
+      name: "PZEMDC Current"
+    power:
+      name: "PZEMDC Power"
 
 time:
 - platform: homeassistant
@@ -246,6 +387,11 @@ binary_sensor:
       - id: custom_binary_sensor
         name: Custom Binary Sensor
 
+globals:
+  - id: my_global_string
+    type: std::string
+    initial_value: '""'
+
 remote_receiver:
   pin: GPIO12
   dump: []
@@ -262,6 +408,8 @@ text_sensor:
       - lambda: !lambda |-
           ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str());
       - script.execute: my_script
+      - script.wait: my_script
+      - script.stop: my_script
       - homeassistant.service:
           service: notify.html5
           data:
@@ -300,12 +448,19 @@ switch:
   - platform: gpio
     id: gpio_switch1
     pin:
-      mcp23017: mcp
+      mcp23017: mcp23017_hub
       number: 0
       mode: OUTPUT
-    interlock: &interlock [gpio_switch1, gpio_switch2]
+    interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3]
   - platform: gpio
     id: gpio_switch2
+    pin:
+      mcp23008: mcp23008_hub
+      number: 0
+      mode: OUTPUT
+    interlock: *interlock
+  - platform: gpio
+    id: gpio_switch3
     pin: GPIO1
     interlock: *interlock
   - platform: custom
@@ -416,6 +571,14 @@ cover:
     close_action:
       - switch.turn_on: gpio_switch2
     close_duration: 4.5min
+  - platform: template
+    name: Template Cover with Tilt
+    tilt_lambda: "return 0.5;"
+    tilt_action:
+      - output.set_level:
+          id: out
+          level: !lambda "return tilt;"
+
 
 output:
   - platform: esp8266_pwm
@@ -440,7 +603,10 @@ output:
       - id: custom_float
 
 mcp23017:
-  id: mcp
+  id: mcp23017_hub
+  
+mcp23008:
+  id: mcp23008_hub
 
 light:
   - platform: neopixelbus
@@ -460,3 +626,23 @@ ttp229_lsf:
 ttp229_bsf:
   sdo_pin: D0
   scl_pin: D1
+
+sim800l:
+  on_sms_received:
+    - lambda: |-
+        std::string str;
+        str = sender;
+        str = message;
+    - sim800l.send_sms:
+        message: 'hello you'
+        recipient: '+1234'
+
+dfplayer:
+  on_finished_playback:
+    then:
+      if:
+        condition:
+          not:
+            dfplayer.is_playing
+        then:
+          logger.log: 'Playback finished event'