diff --git a/.coveragerc b/.coveragerc
index 01c0a657f31e8653fa88655623777461a6119774..a8459a2cd74762b3b8b95c7f30c8cea1f8468670 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -567,6 +567,8 @@ omit =
     homeassistant/components/n26/*
     homeassistant/components/nad/media_player.py
     homeassistant/components/nanoleaf/light.py
+    homeassistant/components/neato/__init__.py
+    homeassistant/components/neato/api.py
     homeassistant/components/neato/camera.py
     homeassistant/components/neato/sensor.py
     homeassistant/components/neato/switch.py
diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py
index 9775dc592fd72a3797e8f934850c8371527131ac..1d9d3de4f89a8034aae80c356648edae2c1da51e 100644
--- a/homeassistant/components/neato/__init__.py
+++ b/homeassistant/components/neato/__init__.py
@@ -3,26 +3,30 @@ import asyncio
 from datetime import timedelta
 import logging
 
-from pybotvac import Account, Neato, Vorwerk
-from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException
+from pybotvac import Account, Neato
+from pybotvac.exceptions import NeatoException
 import voluptuous as vol
 
-from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
+from homeassistant.const import (
+    CONF_CLIENT_ID,
+    CONF_CLIENT_SECRET,
+    CONF_SOURCE,
+    CONF_TOKEN,
+)
 from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
 from homeassistant.util import Throttle
 
-from .config_flow import NeatoConfigFlow
+from . import api, config_flow
 from .const import (
-    CONF_VENDOR,
     NEATO_CONFIG,
     NEATO_DOMAIN,
     NEATO_LOGIN,
     NEATO_MAP_DATA,
     NEATO_PERSISTENT_MAPS,
     NEATO_ROBOTS,
-    VALID_VENDORS,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -32,82 +36,74 @@ CONFIG_SCHEMA = vol.Schema(
     {
         NEATO_DOMAIN: vol.Schema(
             {
-                vol.Required(CONF_USERNAME): cv.string,
-                vol.Required(CONF_PASSWORD): cv.string,
-                vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
+                vol.Required(CONF_CLIENT_ID): cv.string,
+                vol.Required(CONF_CLIENT_SECRET): cv.string,
             }
         )
     },
     extra=vol.ALLOW_EXTRA,
 )
 
+PLATFORMS = ["camera", "vacuum", "switch", "sensor"]
+
 
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
     """Set up the Neato component."""
+    hass.data[NEATO_DOMAIN] = {}
 
     if NEATO_DOMAIN not in config:
-        # There is an entry and nothing in configuration.yaml
         return True
 
-    entries = hass.config_entries.async_entries(NEATO_DOMAIN)
     hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN]
+    vendor = Neato()
+    config_flow.OAuth2FlowHandler.async_register_implementation(
+        hass,
+        api.NeatoImplementation(
+            hass,
+            NEATO_DOMAIN,
+            config[NEATO_DOMAIN][CONF_CLIENT_ID],
+            config[NEATO_DOMAIN][CONF_CLIENT_SECRET],
+            vendor.auth_endpoint,
+            vendor.token_endpoint,
+        ),
+    )
 
-    if entries:
-        # There is an entry and something in the configuration.yaml
-        entry = entries[0]
-        conf = config[NEATO_DOMAIN]
-        if (
-            entry.data[CONF_USERNAME] == conf[CONF_USERNAME]
-            and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD]
-            and entry.data[CONF_VENDOR] == conf[CONF_VENDOR]
-        ):
-            # The entry is not outdated
-            return True
-
-        # The entry is outdated
-        error = await hass.async_add_executor_job(
-            NeatoConfigFlow.try_login,
-            conf[CONF_USERNAME],
-            conf[CONF_PASSWORD],
-            conf[CONF_VENDOR],
-        )
-        if error is not None:
-            _LOGGER.error(error)
-            return False
-
-        # Update the entry
-        hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN])
-    else:
-        # Create the new entry
+    return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+    """Set up config entry."""
+    if CONF_TOKEN not in entry.data:
+        # Init reauth flow
         hass.async_create_task(
             hass.config_entries.flow.async_init(
                 NEATO_DOMAIN,
-                context={"source": SOURCE_IMPORT},
-                data=config[NEATO_DOMAIN],
+                context={CONF_SOURCE: SOURCE_REAUTH},
             )
         )
+        return False
 
-    return True
-
+    implementation = (
+        await config_entry_oauth2_flow.async_get_config_entry_implementation(
+            hass, entry
+        )
+    )
 
-async def async_setup_entry(hass, entry):
-    """Set up config entry."""
-    hub = NeatoHub(hass, entry.data, Account)
+    session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
 
-    await hass.async_add_executor_job(hub.login)
-    if not hub.logged_in:
-        _LOGGER.debug("Failed to login to Neato API")
-        return False
+    neato_session = api.ConfigEntryAuth(hass, entry, session)
+    hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session
+    hub = NeatoHub(hass, Account(neato_session))
 
     try:
         await hass.async_add_executor_job(hub.update_robots)
-    except NeatoRobotException as ex:
+    except NeatoException as ex:
         _LOGGER.debug("Failed to connect to Neato API")
         raise ConfigEntryNotReady from ex
 
     hass.data[NEATO_LOGIN] = hub
 
-    for component in ("camera", "vacuum", "switch", "sensor"):
+    for component in PLATFORMS:
         hass.async_create_task(
             hass.config_entries.async_forward_entry_setup(entry, component)
         )
@@ -115,53 +111,27 @@ async def async_setup_entry(hass, entry):
     return True
 
 
-async def async_unload_entry(hass, entry):
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
     """Unload config entry."""
-    hass.data.pop(NEATO_LOGIN)
-    await asyncio.gather(
-        hass.config_entries.async_forward_entry_unload(entry, "camera"),
-        hass.config_entries.async_forward_entry_unload(entry, "vacuum"),
-        hass.config_entries.async_forward_entry_unload(entry, "switch"),
-        hass.config_entries.async_forward_entry_unload(entry, "sensor"),
+    unload_functions = (
+        hass.config_entries.async_forward_entry_unload(entry, platform)
+        for platform in PLATFORMS
     )
-    return True
+
+    unload_ok = all(await asyncio.gather(*unload_functions))
+    if unload_ok:
+        hass.data[NEATO_DOMAIN].pop(entry.entry_id)
+
+    return unload_ok
 
 
 class NeatoHub:
     """A My Neato hub wrapper class."""
 
-    def __init__(self, hass, domain_config, neato):
+    def __init__(self, hass: HomeAssistantType, neato: Account):
         """Initialize the Neato hub."""
-        self.config = domain_config
-        self._neato = neato
-        self._hass = hass
-
-        if self.config[CONF_VENDOR] == "vorwerk":
-            self._vendor = Vorwerk()
-        else:  # Neato
-            self._vendor = Neato()
-
-        self.my_neato = None
-        self.logged_in = False
-
-    def login(self):
-        """Login to My Neato."""
-        _LOGGER.debug("Trying to connect to Neato API")
-        try:
-            self.my_neato = self._neato(
-                self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor
-            )
-        except NeatoException as ex:
-            if isinstance(ex, NeatoLoginException):
-                _LOGGER.error("Invalid credentials")
-            else:
-                _LOGGER.error("Unable to connect to Neato API")
-                raise ConfigEntryNotReady from ex
-            self.logged_in = False
-            return
-
-        self.logged_in = True
-        _LOGGER.debug("Successfully connected to Neato API")
+        self._hass: HomeAssistantType = hass
+        self.my_neato: Account = neato
 
     @Throttle(timedelta(minutes=1))
     def update_robots(self):
diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py
new file mode 100644
index 0000000000000000000000000000000000000000..931d7cdb712be14d64442cf6e5f5a27c3cdc71c8
--- /dev/null
+++ b/homeassistant/components/neato/api.py
@@ -0,0 +1,55 @@
+"""API for Neato Botvac bound to Home Assistant OAuth."""
+from asyncio import run_coroutine_threadsafe
+import logging
+
+import pybotvac
+
+from homeassistant import config_entries, core
+from homeassistant.helpers import config_entry_oauth2_flow
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigEntryAuth(pybotvac.OAuthSession):
+    """Provide Neato Botvac authentication tied to an OAuth2 based config entry."""
+
+    def __init__(
+        self,
+        hass: core.HomeAssistant,
+        config_entry: config_entries.ConfigEntry,
+        implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
+    ):
+        """Initialize Neato Botvac Auth."""
+        self.hass = hass
+        self.session = config_entry_oauth2_flow.OAuth2Session(
+            hass, config_entry, implementation
+        )
+        super().__init__(self.session.token, vendor=pybotvac.Neato())
+
+    def refresh_tokens(self) -> str:
+        """Refresh and return new Neato Botvac tokens using Home Assistant OAuth2 session."""
+        run_coroutine_threadsafe(
+            self.session.async_ensure_token_valid(), self.hass.loop
+        ).result()
+
+        return self.session.token["access_token"]
+
+
+class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation):
+    """Neato implementation of LocalOAuth2Implementation.
+
+    We need this class because we have to add client_secret and scope to the authorization request.
+    """
+
+    @property
+    def extra_authorize_data(self) -> dict:
+        """Extra data that needs to be appended to the authorize url."""
+        return {"client_secret": self.client_secret}
+
+    async def async_generate_authorize_url(self, flow_id: str) -> str:
+        """Generate a url for the user to authorize.
+
+        We must make sure that the plus signs are not encoded.
+        """
+        url = await super().async_generate_authorize_url(flow_id)
+        return f"{url}&scope=public_profile+control_robots+maps"
diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py
index 4d7c4129d818786ee649f0ece99d177e2a96b38f..1698a1d944a6e6e2af98fe54b1ac38a34f2d2019 100644
--- a/homeassistant/components/neato/camera.py
+++ b/homeassistant/components/neato/camera.py
@@ -45,7 +45,7 @@ class NeatoCleaningMap(Camera):
         self.robot = robot
         self.neato = neato
         self._mapdata = mapdata
-        self._available = self.neato.logged_in if self.neato is not None else False
+        self._available = neato is not None
         self._robot_name = f"{self.robot.name} Cleaning Map"
         self._robot_serial = self.robot.serial
         self._generated_at = None
diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py
index f74364bc8bc38dd5a0a1cf3481116cccaa4295fd..449de72b1589085ec560d4aa18c4fd7ca839dc08 100644
--- a/homeassistant/components/neato/config_flow.py
+++ b/homeassistant/components/neato/config_flow.py
@@ -1,112 +1,65 @@
-"""Config flow to configure Neato integration."""
-
+"""Config flow for Neato Botvac."""
 import logging
+from typing import Optional
 
-from pybotvac import Account, Neato, Vorwerk
-from pybotvac.exceptions import NeatoLoginException, NeatoRobotException
 import voluptuous as vol
 
 from homeassistant import config_entries
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_TOKEN
+from homeassistant.helpers import config_entry_oauth2_flow
 
 # pylint: disable=unused-import
-from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS
-
-DOCS_URL = "https://www.home-assistant.io/integrations/neato"
-DEFAULT_VENDOR = "neato"
+from .const import NEATO_DOMAIN
 
 _LOGGER = logging.getLogger(__name__)
 
 
-class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN):
-    """Neato integration config flow."""
+class OAuth2FlowHandler(
+    config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=NEATO_DOMAIN
+):
+    """Config flow to handle Neato Botvac OAuth2 authentication."""
 
-    VERSION = 1
+    DOMAIN = NEATO_DOMAIN
     CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
 
-    def __init__(self):
-        """Initialize flow."""
-        self._username = vol.UNDEFINED
-        self._password = vol.UNDEFINED
-        self._vendor = vol.UNDEFINED
-
-    async def async_step_user(self, user_input=None):
-        """Handle a flow initialized by the user."""
-        errors = {}
-
-        if self._async_current_entries():
-            return self.async_abort(reason="already_configured")
-
-        if user_input is not None:
-            self._username = user_input["username"]
-            self._password = user_input["password"]
-            self._vendor = user_input["vendor"]
-
-            error = await self.hass.async_add_executor_job(
-                self.try_login, self._username, self._password, self._vendor
-            )
-            if error:
-                errors["base"] = error
-            else:
-                return self.async_create_entry(
-                    title=user_input[CONF_USERNAME],
-                    data=user_input,
-                    description_placeholders={"docs_url": DOCS_URL},
-                )
-
-        return self.async_show_form(
-            step_id="user",
-            data_schema=vol.Schema(
-                {
-                    vol.Required(CONF_USERNAME): str,
-                    vol.Required(CONF_PASSWORD): str,
-                    vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
-                }
-            ),
-            description_placeholders={"docs_url": DOCS_URL},
-            errors=errors,
-        )
-
-    async def async_step_import(self, user_input):
-        """Import a config flow from configuration."""
+    @property
+    def logger(self) -> logging.Logger:
+        """Return logger."""
+        return logging.getLogger(__name__)
 
-        if self._async_current_entries():
+    async def async_step_user(self, user_input: Optional[dict] = None) -> dict:
+        """Create an entry for the flow."""
+        current_entries = self._async_current_entries()
+        if current_entries and CONF_TOKEN in current_entries[0].data:
+            # Already configured
             return self.async_abort(reason="already_configured")
 
-        username = user_input[CONF_USERNAME]
-        password = user_input[CONF_PASSWORD]
-        vendor = user_input[CONF_VENDOR]
-
-        error = await self.hass.async_add_executor_job(
-            self.try_login, username, password, vendor
-        )
-        if error is not None:
-            _LOGGER.error(error)
-            return self.async_abort(reason=error)
-
-        return self.async_create_entry(
-            title=f"{username} (from configuration)",
-            data={
-                CONF_USERNAME: username,
-                CONF_PASSWORD: password,
-                CONF_VENDOR: vendor,
-            },
-        )
+        return await super().async_step_user(user_input=user_input)
 
-    @staticmethod
-    def try_login(username, password, vendor):
-        """Try logging in to device and return any errors."""
-        this_vendor = None
-        if vendor == "vorwerk":
-            this_vendor = Vorwerk()
-        else:  # Neato
-            this_vendor = Neato()
+    async def async_step_reauth(self, data) -> dict:
+        """Perform reauth upon migration of old entries."""
+        return await self.async_step_reauth_confirm()
 
-        try:
-            Account(username, password, this_vendor)
-        except NeatoLoginException:
-            return "invalid_auth"
-        except NeatoRobotException:
-            return "unknown"
-
-        return None
+    async def async_step_reauth_confirm(
+        self, user_input: Optional[dict] = None
+    ) -> dict:
+        """Confirm reauth upon migration of old entries."""
+        if user_input is None:
+            return self.async_show_form(
+                step_id="reauth_confirm", data_schema=vol.Schema({})
+            )
+        return await self.async_step_user()
+
+    async def async_oauth_create_entry(self, data: dict) -> dict:
+        """Create an entry for the flow. Update an entry if one already exist."""
+        current_entries = self._async_current_entries()
+        if current_entries and CONF_TOKEN not in current_entries[0].data:
+            # Update entry
+            self.hass.config_entries.async_update_entry(
+                current_entries[0], title=self.flow_impl.name, data=data
+            )
+            self.hass.async_create_task(
+                self.hass.config_entries.async_reload(current_entries[0].entry_id)
+            )
+            return self.async_abort(reason="reauth_successful")
+        return self.async_create_entry(title=self.flow_impl.name, data=data)
diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py
index 53948e2b19dbd3cf8c9f05f80ae74b4065bbffd5..248e455b6da62f7f5e588676432050090bf4f716 100644
--- a/homeassistant/components/neato/const.py
+++ b/homeassistant/components/neato/const.py
@@ -11,8 +11,6 @@ NEATO_ROBOTS = "neato_robots"
 
 SCAN_INTERVAL_MINUTES = 1
 
-VALID_VENDORS = ["neato", "vorwerk"]
-
 MODE = {1: "Eco", 2: "Turbo"}
 
 ACTION = {
diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json
index d36e3fa503fbf8721b48ba97a2319ccc452a5f49..d3ea8a8525cb435557c38aab18873dd9797efe36 100644
--- a/homeassistant/components/neato/manifest.json
+++ b/homeassistant/components/neato/manifest.json
@@ -3,6 +3,14 @@
   "name": "Neato Botvac",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/neato",
-  "requirements": ["pybotvac==0.0.17"],
-  "codeowners": ["@dshokouhi", "@Santobert"]
-}
+  "requirements": [
+    "pybotvac==0.0.19"
+  ],
+  "codeowners": [
+    "@dshokouhi",
+    "@Santobert"
+  ],
+  "dependencies": [
+    "http"
+  ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py
index efcbfb8d54c1e4ae134541dad177093dbaeb5ce6..b083ec1d7dfdbc232688ee9c16200b021c9deaa6 100644
--- a/homeassistant/components/neato/sensor.py
+++ b/homeassistant/components/neato/sensor.py
@@ -37,7 +37,7 @@ class NeatoSensor(Entity):
     def __init__(self, neato, robot):
         """Initialize Neato sensor."""
         self.robot = robot
-        self._available = neato.logged_in if neato is not None else False
+        self._available = neato is not None
         self._robot_name = f"{self.robot.name} {BATTERY}"
         self._robot_serial = self.robot.serial
         self._state = None
diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json
index 5d71d4889acc04164761db5b3a4fc6514f9aa86d..21af0f91d17ec9465b926ffb1dd306e8b6cc2014 100644
--- a/homeassistant/components/neato/strings.json
+++ b/homeassistant/components/neato/strings.json
@@ -1,26 +1,23 @@
 {
   "config": {
     "step": {
-      "user": {
-        "title": "Neato Account Info",
-        "data": {
-          "username": "[%key:common::config_flow::data::username%]",
-          "password": "[%key:common::config_flow::data::password%]",
-          "vendor": "Vendor"
-        },
-        "description": "See [Neato documentation]({docs_url})."
+      "pick_implementation": {
+        "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
+      },
+      "reauth_confirm": {
+        "title": "[%key:common::config_flow::description::confirm_setup%]"
       }
     },
-    "error": {
-      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
-      "unknown": "[%key:common::config_flow::error::unknown%]"
-    },
-    "create_entry": {
-      "default": "See [Neato documentation]({docs_url})."
-    },
     "abort": {
+      "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
+      "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
+      "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
-      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+      "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+    },
+    "create_entry": {
+      "default": "[%key:common::config_flow::create_entry::authenticated%]"
     }
-  }
+  },
+  "title": "Neato Botvac"
 }
\ No newline at end of file
diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py
index a6aa19abe26c9b3493d395fc557c7e5dcdb4dbe0..204adb108a830e4e73ae10b29ac1459ebfff1a3a 100644
--- a/homeassistant/components/neato/switch.py
+++ b/homeassistant/components/neato/switch.py
@@ -40,7 +40,7 @@ class NeatoConnectedSwitch(ToggleEntity):
         """Initialize the Neato Connected switches."""
         self.type = switch_type
         self.robot = robot
-        self._available = neato.logged_in if neato is not None else False
+        self._available = neato is not None
         self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}"
         self._state = None
         self._schedule_state = None
diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json
index c41d4e6d93a0ec11bb41cda772ca70e8ce65981f..c8dcc93500b19478d55e6527dc08c6009f89cfc3 100644
--- a/homeassistant/components/neato/translations/de.json
+++ b/homeassistant/components/neato/translations/de.json
@@ -1,21 +1,23 @@
 {
     "config": {
         "abort": {
-            "already_configured": "Bereits konfiguriert"
+            "already_configured": "Konto ist bereits konfiguriert.",
+            "authorize_url_timeout": "Timeout beim Erzeugen der Autorisierungs-URL.",
+            "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachten Sie die Dokumentation.",
+            "no_url_available": "Keine URL verfügbar. Informationen zu diesem Fehler finden Sie [im Hilfebereich]({docs_url})",
+            "reauth_successful": "Re-Authentifizierung war erfolgreich"
         },
         "create_entry": {
-            "default": "Siehe [Neato-Dokumentation]({docs_url})."
+            "default": "Erfolgreich authentifiziert"
         },
         "step": {
-            "user": {
-                "data": {
-                    "password": "Passwort",
-                    "username": "Benutzername",
-                    "vendor": "Hersteller"
-                },
-                "description": "Siehe [Neato-Dokumentation]({docs_url}).",
-                "title": "Neato-Kontoinformationen"
+            "pick_implementation": {
+                "title": "Authentifizierungsmethode auswählen"
+            },
+            "reauth_confirm": {
+                "title": "Einrichtung bestätigen?"
             }
         }
-    }
+    },
+    "title": "Neato Botvac"
 }
\ No newline at end of file
diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json
index 61b6ad44dfd9f4846968cf645bafa01c170d3fc6..333c8a980f0b3b9da653c0fd3d86784dd215e13f 100644
--- a/homeassistant/components/neato/translations/en.json
+++ b/homeassistant/components/neato/translations/en.json
@@ -1,26 +1,23 @@
 {
     "config": {
         "abort": {
-            "already_configured": "Device is already configured",
-            "invalid_auth": "Invalid authentication"
+            "already_configured": "Account is already configured.",
+            "authorize_url_timeout": "Timeout generating authorize URL.",
+            "missing_configuration": "The component is not configured. Please follow the documentation.",
+            "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+            "reauth_successful": "Re-authentication was successful"
         },
         "create_entry": {
-            "default": "See [Neato documentation]({docs_url})."
-        },
-        "error": {
-            "invalid_auth": "Invalid authentication",
-            "unknown": "Unexpected error"
+            "default": "Successfully authenticated"
         },
         "step": {
-            "user": {
-                "data": {
-                    "password": "Password",
-                    "username": "Username",
-                    "vendor": "Vendor"
-                },
-                "description": "See [Neato documentation]({docs_url}).",
-                "title": "Neato Account Info"
+            "pick_implementation": {
+                "title": "Pick Authentication Method"
+            },
+            "reauth_confirm": {
+                "title": "Confirm setup?"
             }
         }
-    }
+    },
+    "title": "Neato Botvac"
 }
\ No newline at end of file
diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py
index 677bed1565b491df661c1c831489dd44b0b866bf..ce4156244b7c07c678d7da6dea29828fab420f6d 100644
--- a/homeassistant/components/neato/vacuum.py
+++ b/homeassistant/components/neato/vacuum.py
@@ -24,7 +24,7 @@ from homeassistant.components.vacuum import (
     SUPPORT_STOP,
     StateVacuumEntity,
 )
-from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
+from homeassistant.const import ATTR_MODE
 from homeassistant.helpers import config_validation as cv, entity_platform
 
 from .const import (
@@ -93,7 +93,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
     platform.async_register_entity_service(
         "custom_cleaning",
         {
-            vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
             vol.Optional(ATTR_MODE, default=2): cv.positive_int,
             vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int,
             vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int,
@@ -109,7 +108,7 @@ class NeatoConnectedVacuum(StateVacuumEntity):
     def __init__(self, neato, robot, mapdata, persistent_maps):
         """Initialize the Neato Connected Vacuum."""
         self.robot = robot
-        self._available = neato.logged_in if neato is not None else False
+        self._available = neato is not None
         self._mapdata = mapdata
         self._name = f"{self.robot.name}"
         self._robot_has_map = self.robot.has_persistent_maps
diff --git a/requirements_all.txt b/requirements_all.txt
index 80f685845aec66e55f636d36654e4413866206f8..20e3c4c115c7146a437633bb4240e5163911a626 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1292,7 +1292,7 @@ pyblackbird==0.5
 # pybluez==0.22
 
 # homeassistant.components.neato
-pybotvac==0.0.17
+pybotvac==0.0.19
 
 # homeassistant.components.nissan_leaf
 pycarwings2==2.9
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index fef2cba80d05a2daf9dd3d1effe358a748ac2582..27fe91d0eb434ab6b7237cff7d53e99726936f82 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -655,7 +655,7 @@ pyatv==0.7.5
 pyblackbird==0.5
 
 # homeassistant.components.neato
-pybotvac==0.0.17
+pybotvac==0.0.19
 
 # homeassistant.components.cloudflare
 pycfdns==1.2.1
diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py
index 31c2cddd09d9ed724af3ff64449aaea2d2c41987..6954eb1b7afb6f9aea29c043d1b00ed55f860ccf 100644
--- a/tests/components/neato/test_config_flow.py
+++ b/tests/components/neato/test_config_flow.py
@@ -1,160 +1,156 @@
-"""Tests for the Neato config flow."""
-from pybotvac.exceptions import NeatoLoginException, NeatoRobotException
-import pytest
+"""Test the Neato Botvac config flow."""
+from pybotvac.neato import Neato
 
-from homeassistant import data_entry_flow
-from homeassistant.components.neato import config_flow
-from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.neato.const import NEATO_DOMAIN
+from homeassistant.helpers import config_entry_oauth2_flow
+from homeassistant.helpers.typing import HomeAssistantType
 
 from tests.async_mock import patch
 from tests.common import MockConfigEntry
 
-USERNAME = "myUsername"
-PASSWORD = "myPassword"
-VENDOR_NEATO = "neato"
-VENDOR_VORWERK = "vorwerk"
-VENDOR_INVALID = "invalid"
+CLIENT_ID = "1234"
+CLIENT_SECRET = "5678"
 
+VENDOR = Neato()
+OAUTH2_AUTHORIZE = VENDOR.auth_endpoint
+OAUTH2_TOKEN = VENDOR.token_endpoint
 
-@pytest.fixture(name="account")
-def mock_controller_login():
-    """Mock a successful login."""
-    with patch("homeassistant.components.neato.config_flow.Account", return_value=True):
-        yield
 
+async def test_full_flow(
+    hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
+    """Check full flow."""
+    assert await setup.async_setup_component(
+        hass,
+        "neato",
+        {
+            "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
+            "http": {"base_url": "https://example.com"},
+        },
+    )
 
-def init_config_flow(hass):
-    """Init a configuration flow."""
-    flow = config_flow.NeatoConfigFlow()
-    flow.hass = hass
-    return flow
-
-
-async def test_user(hass, account):
-    """Test user config."""
-    flow = init_config_flow(hass)
-
-    result = await flow.async_step_user()
-    assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
-    assert result["step_id"] == "user"
-
-    result = await flow.async_step_user(
-        {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+    result = await hass.config_entries.flow.async_init(
+        "neato", context={"source": config_entries.SOURCE_USER}
+    )
+    state = config_entry_oauth2_flow._encode_jwt(
+        hass,
+        {
+            "flow_id": result["flow_id"],
+            "redirect_uri": "https://example.com/auth/external/callback",
+        },
     )
 
-    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
-    assert result["title"] == USERNAME
-    assert result["data"][CONF_USERNAME] == USERNAME
-    assert result["data"][CONF_PASSWORD] == PASSWORD
-    assert result["data"][CONF_VENDOR] == VENDOR_NEATO
+    assert result["url"] == (
+        f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
+        "&redirect_uri=https://example.com/auth/external/callback"
+        f"&state={state}"
+        f"&client_secret={CLIENT_SECRET}"
+        "&scope=public_profile+control_robots+maps"
+    )
 
-    result = await flow.async_step_user(
-        {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK}
+    client = await aiohttp_client(hass.http.app)
+    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+    assert resp.status == 200
+    assert resp.headers["content-type"] == "text/html; charset=utf-8"
+
+    aioclient_mock.post(
+        OAUTH2_TOKEN,
+        json={
+            "refresh_token": "mock-refresh-token",
+            "access_token": "mock-access-token",
+            "type": "Bearer",
+            "expires_in": 60,
+        },
     )
 
-    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
-    assert result["title"] == USERNAME
-    assert result["data"][CONF_USERNAME] == USERNAME
-    assert result["data"][CONF_PASSWORD] == PASSWORD
-    assert result["data"][CONF_VENDOR] == VENDOR_VORWERK
+    with patch(
+        "homeassistant.components.neato.async_setup_entry", return_value=True
+    ) as mock_setup:
+        await hass.config_entries.flow.async_configure(result["flow_id"])
+
+    assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1
+    assert len(mock_setup.mock_calls) == 1
 
 
-async def test_import(hass, account):
-    """Test import step."""
-    flow = init_config_flow(hass)
+async def test_abort_if_already_setup(hass: HomeAssistantType):
+    """Test we abort if Neato is already setup."""
+    entry = MockConfigEntry(
+        domain=NEATO_DOMAIN,
+        data={"auth_implementation": "neato", "token": {"some": "data"}},
+    )
+    entry.add_to_hass(hass)
 
-    result = await flow.async_step_import(
-        {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+    # Should fail
+    result = await hass.config_entries.flow.async_init(
+        "neato", context={"source": config_entries.SOURCE_USER}
     )
+    assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+    assert result["reason"] == "already_configured"
 
-    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
-    assert result["title"] == f"{USERNAME} (from configuration)"
-    assert result["data"][CONF_USERNAME] == USERNAME
-    assert result["data"][CONF_PASSWORD] == PASSWORD
-    assert result["data"][CONF_VENDOR] == VENDOR_NEATO
 
+async def test_reauth(
+    hass: HomeAssistantType, aiohttp_client, aioclient_mock, current_request_with_host
+):
+    """Test initialization of the reauth flow."""
+    assert await setup.async_setup_component(
+        hass,
+        "neato",
+        {
+            "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
+            "http": {"base_url": "https://example.com"},
+        },
+    )
 
-async def test_abort_if_already_setup(hass, account):
-    """Test we abort if Neato is already setup."""
-    flow = init_config_flow(hass)
     MockConfigEntry(
+        entry_id="my_entry",
         domain=NEATO_DOMAIN,
-        data={
-            CONF_USERNAME: USERNAME,
-            CONF_PASSWORD: PASSWORD,
-            CONF_VENDOR: VENDOR_NEATO,
-        },
+        data={"username": "abcdef", "password": "123456", "vendor": "neato"},
     ).add_to_hass(hass)
 
-    # Should fail, same USERNAME (import)
-    result = await flow.async_step_import(
-        {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
-    )
-    assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-    assert result["reason"] == "already_configured"
-
-    # Should fail, same USERNAME (flow)
-    result = await flow.async_step_user(
-        {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
+    # Should show form
+    result = await hass.config_entries.flow.async_init(
+        "neato", context={"source": config_entries.SOURCE_REAUTH}
     )
-    assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-    assert result["reason"] == "already_configured"
+    assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+    assert result["step_id"] == "reauth_confirm"
 
+    # Confirm reauth flow
+    result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
 
-async def test_abort_on_invalid_credentials(hass):
-    """Test when we have invalid credentials."""
-    flow = init_config_flow(hass)
+    state = config_entry_oauth2_flow._encode_jwt(
+        hass,
+        {
+            "flow_id": result["flow_id"],
+            "redirect_uri": "https://example.com/auth/external/callback",
+        },
+    )
 
-    with patch(
-        "homeassistant.components.neato.config_flow.Account",
-        side_effect=NeatoLoginException(),
-    ):
-        result = await flow.async_step_user(
-            {
-                CONF_USERNAME: USERNAME,
-                CONF_PASSWORD: PASSWORD,
-                CONF_VENDOR: VENDOR_NEATO,
-            }
-        )
-        assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
-        assert result["errors"] == {"base": "invalid_auth"}
-
-        result = await flow.async_step_import(
-            {
-                CONF_USERNAME: USERNAME,
-                CONF_PASSWORD: PASSWORD,
-                CONF_VENDOR: VENDOR_NEATO,
-            }
-        )
-        assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-        assert result["reason"] == "invalid_auth"
-
-
-async def test_abort_on_unexpected_error(hass):
-    """Test when we have an unexpected error."""
-    flow = init_config_flow(hass)
+    client = await aiohttp_client(hass.http.app)
+    resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
+    assert resp.status == 200
+
+    aioclient_mock.post(
+        OAUTH2_TOKEN,
+        json={
+            "refresh_token": "mock-refresh-token",
+            "access_token": "mock-access-token",
+            "type": "Bearer",
+            "expires_in": 60,
+        },
+    )
 
+    # Update entry
     with patch(
-        "homeassistant.components.neato.config_flow.Account",
-        side_effect=NeatoRobotException(),
-    ):
-        result = await flow.async_step_user(
-            {
-                CONF_USERNAME: USERNAME,
-                CONF_PASSWORD: PASSWORD,
-                CONF_VENDOR: VENDOR_NEATO,
-            }
-        )
-        assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
-        assert result["errors"] == {"base": "unknown"}
-
-        result = await flow.async_step_import(
-            {
-                CONF_USERNAME: USERNAME,
-                CONF_PASSWORD: PASSWORD,
-                CONF_VENDOR: VENDOR_NEATO,
-            }
-        )
-        assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
-        assert result["reason"] == "unknown"
+        "homeassistant.components.neato.async_setup_entry", return_value=True
+    ) as mock_setup:
+        result3 = await hass.config_entries.flow.async_configure(result2["flow_id"])
+        await hass.async_block_till_done()
+
+    new_entry = hass.config_entries.async_get_entry("my_entry")
+
+    assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT
+    assert result3["reason"] == "reauth_successful"
+    assert new_entry.state == "loaded"
+    assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1
+    assert len(mock_setup.mock_calls) == 1
diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py
deleted file mode 100644
index 182ef98e52923eb7550f5098efb4bfc26f2114f1..0000000000000000000000000000000000000000
--- a/tests/components/neato/test_init.py
+++ /dev/null
@@ -1,118 +0,0 @@
-"""Tests for the Neato init file."""
-from pybotvac.exceptions import NeatoLoginException
-import pytest
-
-from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from homeassistant.setup import async_setup_component
-
-from tests.async_mock import patch
-from tests.common import MockConfigEntry
-
-USERNAME = "myUsername"
-PASSWORD = "myPassword"
-VENDOR_NEATO = "neato"
-VENDOR_VORWERK = "vorwerk"
-VENDOR_INVALID = "invalid"
-
-VALID_CONFIG = {
-    CONF_USERNAME: USERNAME,
-    CONF_PASSWORD: PASSWORD,
-    CONF_VENDOR: VENDOR_NEATO,
-}
-
-DIFFERENT_CONFIG = {
-    CONF_USERNAME: "anotherUsername",
-    CONF_PASSWORD: "anotherPassword",
-    CONF_VENDOR: VENDOR_VORWERK,
-}
-
-INVALID_CONFIG = {
-    CONF_USERNAME: USERNAME,
-    CONF_PASSWORD: PASSWORD,
-    CONF_VENDOR: VENDOR_INVALID,
-}
-
-
-@pytest.fixture(name="config_flow")
-def mock_config_flow_login():
-    """Mock a successful login."""
-    with patch("homeassistant.components.neato.config_flow.Account", return_value=True):
-        yield
-
-
-@pytest.fixture(name="hub")
-def mock_controller_login():
-    """Mock a successful login."""
-    with patch("homeassistant.components.neato.Account", return_value=True):
-        yield
-
-
-async def test_no_config_entry(hass):
-    """There is nothing in configuration.yaml."""
-    res = await async_setup_component(hass, NEATO_DOMAIN, {})
-    assert res is True
-
-
-async def test_create_valid_config_entry(hass, config_flow, hub):
-    """There is something in configuration.yaml."""
-    assert hass.config_entries.async_entries(NEATO_DOMAIN) == []
-    assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
-    await hass.async_block_till_done()
-
-    entries = hass.config_entries.async_entries(NEATO_DOMAIN)
-    assert entries
-    assert entries[0].data[CONF_USERNAME] == USERNAME
-    assert entries[0].data[CONF_PASSWORD] == PASSWORD
-    assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
-
-
-async def test_config_entries_in_sync(hass, hub):
-    """The config entry and configuration.yaml are in sync."""
-    MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass)
-
-    assert hass.config_entries.async_entries(NEATO_DOMAIN)
-    assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
-    await hass.async_block_till_done()
-
-    entries = hass.config_entries.async_entries(NEATO_DOMAIN)
-    assert entries
-    assert entries[0].data[CONF_USERNAME] == USERNAME
-    assert entries[0].data[CONF_PASSWORD] == PASSWORD
-    assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
-
-
-async def test_config_entries_not_in_sync(hass, config_flow, hub):
-    """The config entry and configuration.yaml are not in sync."""
-    MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass)
-
-    assert hass.config_entries.async_entries(NEATO_DOMAIN)
-    assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
-    await hass.async_block_till_done()
-
-    entries = hass.config_entries.async_entries(NEATO_DOMAIN)
-    assert entries
-    assert entries[0].data[CONF_USERNAME] == USERNAME
-    assert entries[0].data[CONF_PASSWORD] == PASSWORD
-    assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
-
-
-async def test_config_entries_not_in_sync_error(hass):
-    """The config entry and configuration.yaml are not in sync, the new configuration is wrong."""
-    MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass)
-
-    assert hass.config_entries.async_entries(NEATO_DOMAIN)
-    with patch(
-        "homeassistant.components.neato.config_flow.Account",
-        side_effect=NeatoLoginException(),
-    ):
-        assert not await async_setup_component(
-            hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG}
-        )
-    await hass.async_block_till_done()
-
-    entries = hass.config_entries.async_entries(NEATO_DOMAIN)
-    assert entries
-    assert entries[0].data[CONF_USERNAME] == USERNAME
-    assert entries[0].data[CONF_PASSWORD] == PASSWORD
-    assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO