diff --git a/.coveragerc b/.coveragerc
index 47d9c84ba0ec1dd63bf81a7f120a7e11b1c0d772..3a274cd004f8b40832cd87a6a07dfea8fbda3db3 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -578,7 +578,20 @@ omit =
     homeassistant/components/mychevy/*
     homeassistant/components/mycroft/*
     homeassistant/components/mycroft/notify.py
-    homeassistant/components/mysensors/*
+    homeassistant/components/mysensors/__init__.py
+    homeassistant/components/mysensors/binary_sensor.py
+    homeassistant/components/mysensors/climate.py
+    homeassistant/components/mysensors/const.py
+    homeassistant/components/mysensors/cover.py
+    homeassistant/components/mysensors/device.py
+    homeassistant/components/mysensors/device_tracker.py
+    homeassistant/components/mysensors/gateway.py
+    homeassistant/components/mysensors/handler.py
+    homeassistant/components/mysensors/helpers.py
+    homeassistant/components/mysensors/light.py
+    homeassistant/components/mysensors/notify.py
+    homeassistant/components/mysensors/sensor.py
+    homeassistant/components/mysensors/switch.py
     homeassistant/components/mystrom/binary_sensor.py
     homeassistant/components/mystrom/light.py
     homeassistant/components/mystrom/switch.py
diff --git a/CODEOWNERS b/CODEOWNERS
index efb338dd4b4ccf179735ab6b0a2ee541e5d94d37..8785ce382cb6b10496fdc3e10a8ada07733c7e14 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -288,7 +288,7 @@ homeassistant/components/mpd/* @fabaff
 homeassistant/components/mqtt/* @home-assistant/core @emontnemery
 homeassistant/components/msteams/* @peroyvind
 homeassistant/components/myq/* @bdraco
-homeassistant/components/mysensors/* @MartinHjelmare
+homeassistant/components/mysensors/* @MartinHjelmare @functionpointer
 homeassistant/components/mystrom/* @fabaff
 homeassistant/components/neato/* @dshokouhi @Santobert
 homeassistant/components/nederlandse_spoorwegen/* @YarmoM
diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py
index 43e398b142fd2e51116a0295646093e8c5b4d7c7..25b4d3106da5c91031d8e3e6d1db63277d2f8d4e 100644
--- a/homeassistant/components/mysensors/__init__.py
+++ b/homeassistant/components/mysensors/__init__.py
@@ -1,12 +1,18 @@
 """Connect to a MySensors gateway via pymysensors API."""
+import asyncio
 import logging
+from typing import Callable, Dict, List, Optional, Tuple, Type, Union
 
+from mysensors import BaseAsyncGateway
 import voluptuous as vol
 
+from homeassistant import config_entries
 from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_OPTIMISTIC
 from homeassistant.core import callback
 import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
 
 from .const import (
     ATTR_DEVICES,
@@ -23,9 +29,14 @@ from .const import (
     CONF_VERSION,
     DOMAIN,
     MYSENSORS_GATEWAYS,
+    MYSENSORS_ON_UNLOAD,
+    SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT,
+    DevId,
+    GatewayId,
+    SensorType,
 )
-from .device import get_mysensors_devices
-from .gateway import finish_setup, get_mysensors_gateway, setup_gateways
+from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices
+from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -81,29 +92,38 @@ def deprecated(key):
 
 NODE_SCHEMA = vol.Schema({cv.positive_int: {vol.Required(CONF_NODE_NAME): cv.string}})
 
-GATEWAY_SCHEMA = {
-    vol.Required(CONF_DEVICE): cv.string,
-    vol.Optional(CONF_PERSISTENCE_FILE): vol.All(cv.string, is_persistence_file),
-    vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int,
-    vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
-    vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic,
-    vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic,
-    vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
-}
+GATEWAY_SCHEMA = vol.Schema(
+    vol.All(
+        deprecated(CONF_NODES),
+        {
+            vol.Required(CONF_DEVICE): cv.string,
+            vol.Optional(CONF_PERSISTENCE_FILE): vol.All(
+                cv.string, is_persistence_file
+            ),
+            vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int,
+            vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
+            vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic,
+            vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic,
+            vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
+        },
+    )
+)
 
 CONFIG_SCHEMA = vol.Schema(
     {
         DOMAIN: vol.Schema(
             vol.All(
                 deprecated(CONF_DEBUG),
+                deprecated(CONF_OPTIMISTIC),
+                deprecated(CONF_PERSISTENCE),
                 {
                     vol.Required(CONF_GATEWAYS): vol.All(
                         cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA]
                     ),
-                    vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
-                    vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean,
                     vol.Optional(CONF_RETAIN, default=True): cv.boolean,
                     vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
+                    vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
+                    vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean,
                 },
             )
         )
@@ -112,69 +132,168 @@ CONFIG_SCHEMA = vol.Schema(
 )
 
 
-async def async_setup(hass, config):
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
     """Set up the MySensors component."""
-    gateways = await setup_gateways(hass, config)
+    if DOMAIN not in config or bool(hass.config_entries.async_entries(DOMAIN)):
+        return True
+
+    config = config[DOMAIN]
+    user_inputs = [
+        {
+            CONF_DEVICE: gw[CONF_DEVICE],
+            CONF_BAUD_RATE: gw[CONF_BAUD_RATE],
+            CONF_TCP_PORT: gw[CONF_TCP_PORT],
+            CONF_TOPIC_OUT_PREFIX: gw.get(CONF_TOPIC_OUT_PREFIX, ""),
+            CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""),
+            CONF_RETAIN: config[CONF_RETAIN],
+            CONF_VERSION: config[CONF_VERSION],
+            CONF_PERSISTENCE_FILE: gw.get(CONF_PERSISTENCE_FILE)
+            # nodes config ignored at this time. renaming nodes can now be done from the frontend.
+        }
+        for gw in config[CONF_GATEWAYS]
+    ]
+    user_inputs = [
+        {k: v for k, v in userinput.items() if v is not None}
+        for userinput in user_inputs
+    ]
+
+    # there is an actual configuration in configuration.yaml, so we have to process it
+    for user_input in user_inputs:
+        hass.async_create_task(
+            hass.config_entries.flow.async_init(
+                DOMAIN,
+                context={"source": config_entries.SOURCE_IMPORT},
+                data=user_input,
+            )
+        )
+
+    return True
 
-    if not gateways:
-        _LOGGER.error("No devices could be setup as gateways, check your configuration")
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+    """Set up an instance of the MySensors integration.
+
+    Every instance has a connection to exactly one Gateway.
+    """
+    gateway = await setup_gateway(hass, entry)
+
+    if not gateway:
+        _LOGGER.error("Gateway setup failed for %s", entry.data)
         return False
 
-    hass.data[MYSENSORS_GATEWAYS] = gateways
+    if DOMAIN not in hass.data:
+        hass.data[DOMAIN] = {}
 
-    hass.async_create_task(finish_setup(hass, config, gateways))
+    if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]:
+        hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {}
+    hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway
+
+    async def finish():
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_setup(entry, platform)
+                for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT
+            ]
+        )
+        await finish_setup(hass, entry, gateway)
+
+    hass.async_create_task(finish())
 
     return True
 
 
-def _get_mysensors_name(gateway, node_id, child_id):
-    """Return a name for a node child."""
-    node_name = f"{gateway.sensors[node_id].sketch_name} {node_id}"
-    node_name = next(
-        (
-            node[CONF_NODE_NAME]
-            for conf_id, node in gateway.nodes_config.items()
-            if node.get(CONF_NODE_NAME) is not None and conf_id == node_id
-        ),
-        node_name,
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+    """Remove an instance of the MySensors integration."""
+
+    gateway = get_mysensors_gateway(hass, entry.entry_id)
+
+    unload_ok = all(
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_unload(entry, platform)
+                for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT
+            ]
+        )
     )
-    return f"{node_name} {child_id}"
+    if not unload_ok:
+        return False
+
+    key = MYSENSORS_ON_UNLOAD.format(entry.entry_id)
+    if key in hass.data[DOMAIN]:
+        for fnct in hass.data[DOMAIN][key]:
+            fnct()
+
+    del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id]
+
+    await gw_stop(hass, entry, gateway)
+    return True
+
+
+async def on_unload(
+    hass: HomeAssistantType, entry: Union[ConfigEntry, GatewayId], fnct: Callable
+) -> None:
+    """Register a callback to be called when entry is unloaded.
+
+    This function is used by platforms to cleanup after themselves
+    """
+    if isinstance(entry, GatewayId):
+        uniqueid = entry
+    else:
+        uniqueid = entry.entry_id
+    key = MYSENSORS_ON_UNLOAD.format(uniqueid)
+    if key not in hass.data[DOMAIN]:
+        hass.data[DOMAIN][key] = []
+    hass.data[DOMAIN][key].append(fnct)
 
 
 @callback
 def setup_mysensors_platform(
     hass,
-    domain,
-    discovery_info,
-    device_class,
-    device_args=None,
-    async_add_entities=None,
-):
-    """Set up a MySensors platform."""
+    domain: str,  # hass platform name
+    discovery_info: Optional[Dict[str, List[DevId]]],
+    device_class: Union[Type[MySensorsDevice], Dict[SensorType, Type[MySensorsEntity]]],
+    device_args: Optional[
+        Tuple
+    ] = None,  # extra arguments that will be given to the entity constructor
+    async_add_entities: Callable = None,
+) -> Optional[List[MySensorsDevice]]:
+    """Set up a MySensors platform.
+
+    Sets up a bunch of instances of a single platform that is supported by this integration.
+    The function is given a list of device ids, each one describing an instance to set up.
+    The function is also given a class.
+    A new instance of the class is created for every device id, and the device id is given to the constructor of the class
+    """
     # Only act if called via MySensors by discovery event.
     # Otherwise gateway is not set up.
     if not discovery_info:
+        _LOGGER.debug("Skipping setup due to no discovery info")
         return None
     if device_args is None:
         device_args = ()
-    new_devices = []
-    new_dev_ids = discovery_info[ATTR_DEVICES]
+    new_devices: List[MySensorsDevice] = []
+    new_dev_ids: List[DevId] = discovery_info[ATTR_DEVICES]
     for dev_id in new_dev_ids:
-        devices = get_mysensors_devices(hass, domain)
+        devices: Dict[DevId, MySensorsDevice] = get_mysensors_devices(hass, domain)
         if dev_id in devices:
+            _LOGGER.debug(
+                "Skipping setup of %s for platform %s as it already exists",
+                dev_id,
+                domain,
+            )
             continue
         gateway_id, node_id, child_id, value_type = dev_id
-        gateway = get_mysensors_gateway(hass, gateway_id)
+        gateway: Optional[BaseAsyncGateway] = get_mysensors_gateway(hass, gateway_id)
         if not gateway:
+            _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id)
             continue
         device_class_copy = device_class
         if isinstance(device_class, dict):
             child = gateway.sensors[node_id].children[child_id]
             s_type = gateway.const.Presentation(child.type).name
             device_class_copy = device_class[s_type]
-        name = _get_mysensors_name(gateway, node_id, child_id)
 
-        args_copy = (*device_args, gateway, node_id, child_id, name, value_type)
+        args_copy = (*device_args, gateway_id, gateway, node_id, child_id, value_type)
         devices[dev_id] = device_class_copy(*args_copy)
         new_devices.append(devices[dev_id])
     if new_devices:
diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py
index 4ec3c6e0abd65c5f7d9f44008b25a24fad272293..c4e12d170c01ec93480c5a6949c360a46b98c9fb 100644
--- a/homeassistant/components/mysensors/binary_sensor.py
+++ b/homeassistant/components/mysensors/binary_sensor.py
@@ -1,4 +1,6 @@
 """Support for MySensors binary sensors."""
+from typing import Callable
+
 from homeassistant.components import mysensors
 from homeassistant.components.binary_sensor import (
     DEVICE_CLASS_MOISTURE,
@@ -10,7 +12,13 @@ from homeassistant.components.binary_sensor import (
     DOMAIN,
     BinarySensorEntity,
 )
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import STATE_ON
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
 
 SENSORS = {
     "S_DOOR": "door",
@@ -24,14 +32,30 @@ SENSORS = {
 }
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the mysensors platform for binary sensors."""
-    mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+    hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+    """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+    @callback
+    def async_discover(discovery_info):
+        """Discover and add a MySensors binary_sensor."""
+        mysensors.setup_mysensors_platform(
+            hass,
+            DOMAIN,
+            discovery_info,
+            MySensorsBinarySensor,
+            async_add_entities=async_add_entities,
+        )
+
+    await on_unload(
         hass,
-        DOMAIN,
-        discovery_info,
-        MySensorsBinarySensor,
-        async_add_entities=async_add_entities,
+        config_entry,
+        async_dispatcher_connect(
+            hass,
+            MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+            async_discover,
+        ),
     )
 
 
diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py
index c318ccf7ec614f86a0417b96ff14e1c1eafd0c29..b1916fc4ed104c2e0b12697f81d04cd79afd2ee6 100644
--- a/homeassistant/components/mysensors/climate.py
+++ b/homeassistant/components/mysensors/climate.py
@@ -1,4 +1,6 @@
 """MySensors platform that offers a Climate (MySensors-HVAC) component."""
+from typing import Callable
+
 from homeassistant.components import mysensors
 from homeassistant.components.climate import ClimateEntity
 from homeassistant.components.climate.const import (
@@ -13,7 +15,12 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_TEMPERATURE,
     SUPPORT_TARGET_TEMPERATURE_RANGE,
 )
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
 
 DICT_HA_TO_MYS = {
     HVAC_MODE_AUTO: "AutoChangeOver",
@@ -32,14 +39,29 @@ FAN_LIST = ["Auto", "Min", "Normal", "Max"]
 OPERATION_LIST = [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT]
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the mysensors climate."""
-    mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+    hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+    """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+    async def async_discover(discovery_info):
+        """Discover and add a MySensors climate."""
+        mysensors.setup_mysensors_platform(
+            hass,
+            DOMAIN,
+            discovery_info,
+            MySensorsHVAC,
+            async_add_entities=async_add_entities,
+        )
+
+    await on_unload(
         hass,
-        DOMAIN,
-        discovery_info,
-        MySensorsHVAC,
-        async_add_entities=async_add_entities,
+        config_entry,
+        async_dispatcher_connect(
+            hass,
+            MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+            async_discover,
+        ),
     )
 
 
@@ -62,15 +84,10 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
             features = features | SUPPORT_TARGET_TEMPERATURE
         return features
 
-    @property
-    def assumed_state(self):
-        """Return True if unable to access real state of entity."""
-        return self.gateway.optimistic
-
     @property
     def temperature_unit(self):
         """Return the unit of measurement."""
-        return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT
+        return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
 
     @property
     def current_temperature(self):
@@ -159,7 +176,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
             self.gateway.set_child_value(
                 self.node_id, self.child_id, value_type, value, ack=1
             )
-            if self.gateway.optimistic:
+            if self.assumed_state:
                 # Optimistically assume that device has changed state
                 self._values[value_type] = value
                 self.async_write_ha_state()
@@ -170,7 +187,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
         self.gateway.set_child_value(
             self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that device has changed state
             self._values[set_req.V_HVAC_SPEED] = fan_mode
             self.async_write_ha_state()
@@ -184,7 +201,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
             DICT_HA_TO_MYS[hvac_mode],
             ack=1,
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that device has changed state
             self._values[self.value_type] = hvac_mode
             self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..058b782d208eeff84160097b25b994e48b2ec634
--- /dev/null
+++ b/homeassistant/components/mysensors/config_flow.py
@@ -0,0 +1,300 @@
+"""Config flow for MySensors."""
+import logging
+import os
+from typing import Any, Dict, Optional
+
+from awesomeversion import (
+    AwesomeVersion,
+    AwesomeVersionStrategy,
+    AwesomeVersionStrategyException,
+)
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic
+from homeassistant.components.mysensors import (
+    CONF_DEVICE,
+    DEFAULT_BAUD_RATE,
+    DEFAULT_TCP_PORT,
+    is_persistence_file,
+)
+from homeassistant.config_entries import ConfigEntry
+import homeassistant.helpers.config_validation as cv
+
+from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION
+
+# pylint: disable=unused-import
+from .const import (
+    CONF_BAUD_RATE,
+    CONF_GATEWAY_TYPE,
+    CONF_GATEWAY_TYPE_ALL,
+    CONF_GATEWAY_TYPE_MQTT,
+    CONF_GATEWAY_TYPE_SERIAL,
+    CONF_GATEWAY_TYPE_TCP,
+    CONF_PERSISTENCE_FILE,
+    CONF_TCP_PORT,
+    CONF_TOPIC_IN_PREFIX,
+    CONF_TOPIC_OUT_PREFIX,
+    DOMAIN,
+    ConfGatewayType,
+)
+from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_connect
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def _get_schema_common() -> dict:
+    """Create a schema with options common to all gateway types."""
+    schema = {
+        vol.Required(
+            CONF_VERSION, default="", description={"suggested_value": DEFAULT_VERSION}
+        ): str,
+        vol.Optional(
+            CONF_PERSISTENCE_FILE,
+        ): str,
+    }
+    return schema
+
+
+def _validate_version(version: str) -> Dict[str, str]:
+    """Validate a version string from the user."""
+    version_okay = False
+    try:
+        version_okay = bool(
+            AwesomeVersion.ensure_strategy(
+                version,
+                [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER],
+            )
+        )
+    except AwesomeVersionStrategyException:
+        pass
+    if version_okay:
+        return {}
+    return {CONF_VERSION: "invalid_version"}
+
+
+def _is_same_device(
+    gw_type: ConfGatewayType, user_input: Dict[str, str], entry: ConfigEntry
+):
+    """Check if another ConfigDevice is actually the same as user_input.
+
+    This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding.
+    """
+    if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]:
+        return False
+    if gw_type == CONF_GATEWAY_TYPE_TCP:
+        return entry.data[CONF_TCP_PORT] == user_input[CONF_TCP_PORT]
+    if gw_type == CONF_GATEWAY_TYPE_MQTT:
+        entry_topics = {
+            entry.data[CONF_TOPIC_IN_PREFIX],
+            entry.data[CONF_TOPIC_OUT_PREFIX],
+        }
+        return (
+            user_input.get(CONF_TOPIC_IN_PREFIX) in entry_topics
+            or user_input.get(CONF_TOPIC_OUT_PREFIX) in entry_topics
+        )
+    return True
+
+
+class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a config flow."""
+
+    async def async_step_import(self, user_input: Optional[Dict[str, str]] = None):
+        """Import a config entry.
+
+        This method is called by async_setup and it has already
+        prepared the dict to be compatible with what a user would have
+        entered from the frontend.
+        Therefore we process it as though it came from the frontend.
+        """
+        if user_input[CONF_DEVICE] == MQTT_COMPONENT:
+            user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_MQTT
+        else:
+            try:
+                await self.hass.async_add_executor_job(
+                    is_serial_port, user_input[CONF_DEVICE]
+                )
+            except vol.Invalid:
+                user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_TCP
+            else:
+                user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL
+
+        result: Dict[str, Any] = await self.async_step_user(user_input=user_input)
+        if result["type"] == "form":
+            return self.async_abort(reason=next(iter(result["errors"].values())))
+        return result
+
+    async def async_step_user(self, user_input: Optional[Dict[str, str]] = None):
+        """Create a config entry from frontend user input."""
+        schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)}
+        schema = vol.Schema(schema)
+
+        if user_input is not None:
+            gw_type = user_input[CONF_GATEWAY_TYPE]
+            input_pass = user_input if CONF_DEVICE in user_input else None
+            if gw_type == CONF_GATEWAY_TYPE_MQTT:
+                return await self.async_step_gw_mqtt(input_pass)
+            if gw_type == CONF_GATEWAY_TYPE_TCP:
+                return await self.async_step_gw_tcp(input_pass)
+            if gw_type == CONF_GATEWAY_TYPE_SERIAL:
+                return await self.async_step_gw_serial(input_pass)
+
+        return self.async_show_form(step_id="user", data_schema=schema)
+
+    async def async_step_gw_serial(self, user_input: Optional[Dict[str, str]] = None):
+        """Create config entry for a serial gateway."""
+        errors = {}
+        if user_input is not None:
+            errors.update(
+                await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input)
+            )
+            if not errors:
+                return self.async_create_entry(
+                    title=f"{user_input[CONF_DEVICE]}", data=user_input
+                )
+
+        schema = _get_schema_common()
+        schema[
+            vol.Required(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE)
+        ] = cv.positive_int
+        schema[vol.Required(CONF_DEVICE, default="/dev/ttyACM0")] = str
+
+        schema = vol.Schema(schema)
+        return self.async_show_form(
+            step_id="gw_serial", data_schema=schema, errors=errors
+        )
+
+    async def async_step_gw_tcp(self, user_input: Optional[Dict[str, str]] = None):
+        """Create a config entry for a tcp gateway."""
+        errors = {}
+        if user_input is not None:
+            if CONF_TCP_PORT in user_input:
+                port: int = user_input[CONF_TCP_PORT]
+                if not (0 < port <= 65535):
+                    errors[CONF_TCP_PORT] = "port_out_of_range"
+
+            errors.update(
+                await self.validate_common(CONF_GATEWAY_TYPE_TCP, errors, user_input)
+            )
+            if not errors:
+                return self.async_create_entry(
+                    title=f"{user_input[CONF_DEVICE]}", data=user_input
+                )
+
+        schema = _get_schema_common()
+        schema[vol.Required(CONF_DEVICE, default="127.0.0.1")] = str
+        # Don't use cv.port as that would show a slider *facepalm*
+        schema[vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT)] = vol.Coerce(int)
+
+        schema = vol.Schema(schema)
+        return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors)
+
+    def _check_topic_exists(self, topic: str) -> bool:
+        for other_config in self.hass.config_entries.async_entries(DOMAIN):
+            if topic == other_config.data.get(
+                CONF_TOPIC_IN_PREFIX
+            ) or topic == other_config.data.get(CONF_TOPIC_OUT_PREFIX):
+                return True
+        return False
+
+    async def async_step_gw_mqtt(self, user_input: Optional[Dict[str, str]] = None):
+        """Create a config entry for a mqtt gateway."""
+        errors = {}
+        if user_input is not None:
+            user_input[CONF_DEVICE] = MQTT_COMPONENT
+
+            try:
+                valid_subscribe_topic(user_input[CONF_TOPIC_IN_PREFIX])
+            except vol.Invalid:
+                errors[CONF_TOPIC_IN_PREFIX] = "invalid_subscribe_topic"
+            else:
+                if self._check_topic_exists(user_input[CONF_TOPIC_IN_PREFIX]):
+                    errors[CONF_TOPIC_IN_PREFIX] = "duplicate_topic"
+
+            try:
+                valid_publish_topic(user_input[CONF_TOPIC_OUT_PREFIX])
+            except vol.Invalid:
+                errors[CONF_TOPIC_OUT_PREFIX] = "invalid_publish_topic"
+            if not errors:
+                if (
+                    user_input[CONF_TOPIC_IN_PREFIX]
+                    == user_input[CONF_TOPIC_OUT_PREFIX]
+                ):
+                    errors[CONF_TOPIC_OUT_PREFIX] = "same_topic"
+                elif self._check_topic_exists(user_input[CONF_TOPIC_OUT_PREFIX]):
+                    errors[CONF_TOPIC_OUT_PREFIX] = "duplicate_topic"
+
+            errors.update(
+                await self.validate_common(CONF_GATEWAY_TYPE_MQTT, errors, user_input)
+            )
+            if not errors:
+                return self.async_create_entry(
+                    title=f"{user_input[CONF_DEVICE]}", data=user_input
+                )
+        schema = _get_schema_common()
+        schema[vol.Required(CONF_RETAIN, default=True)] = bool
+        schema[vol.Required(CONF_TOPIC_IN_PREFIX)] = str
+        schema[vol.Required(CONF_TOPIC_OUT_PREFIX)] = str
+
+        schema = vol.Schema(schema)
+        return self.async_show_form(
+            step_id="gw_mqtt", data_schema=schema, errors=errors
+        )
+
+    def _normalize_persistence_file(self, path: str) -> str:
+        return os.path.realpath(os.path.normcase(self.hass.config.path(path)))
+
+    async def validate_common(
+        self,
+        gw_type: ConfGatewayType,
+        errors: Dict[str, str],
+        user_input: Optional[Dict[str, str]] = None,
+    ) -> Dict[str, str]:
+        """Validate parameters common to all gateway types."""
+        if user_input is not None:
+            errors.update(_validate_version(user_input.get(CONF_VERSION)))
+
+            if gw_type != CONF_GATEWAY_TYPE_MQTT:
+                if gw_type == CONF_GATEWAY_TYPE_TCP:
+                    verification_func = is_socket_address
+                else:
+                    verification_func = is_serial_port
+
+                try:
+                    await self.hass.async_add_executor_job(
+                        verification_func, user_input.get(CONF_DEVICE)
+                    )
+                except vol.Invalid:
+                    errors[CONF_DEVICE] = (
+                        "invalid_ip"
+                        if gw_type == CONF_GATEWAY_TYPE_TCP
+                        else "invalid_serial"
+                    )
+            if CONF_PERSISTENCE_FILE in user_input:
+                try:
+                    is_persistence_file(user_input[CONF_PERSISTENCE_FILE])
+                except vol.Invalid:
+                    errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file"
+                else:
+                    real_persistence_path = self._normalize_persistence_file(
+                        user_input[CONF_PERSISTENCE_FILE]
+                    )
+                    for other_entry in self.hass.config_entries.async_entries(DOMAIN):
+                        if CONF_PERSISTENCE_FILE not in other_entry.data:
+                            continue
+                        if real_persistence_path == self._normalize_persistence_file(
+                            other_entry.data[CONF_PERSISTENCE_FILE]
+                        ):
+                            errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file"
+                            break
+
+            for other_entry in self.hass.config_entries.async_entries(DOMAIN):
+                if _is_same_device(gw_type, user_input, other_entry):
+                    errors["base"] = "already_configured"
+                    break
+
+            # if no errors so far, try to connect
+            if not errors and not await try_connect(self.hass, user_input):
+                errors["base"] = "cannot_connect"
+
+        return errors
diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py
index ccb646eb47eec6ec65a537efec82944464445fbe..66bee128d4d27eb18a1cfa2375abbd44fe1a08f3 100644
--- a/homeassistant/components/mysensors/const.py
+++ b/homeassistant/components/mysensors/const.py
@@ -1,33 +1,69 @@
 """MySensors constants."""
 from collections import defaultdict
-
-ATTR_DEVICES = "devices"
-
-CONF_BAUD_RATE = "baud_rate"
-CONF_DEVICE = "device"
-CONF_GATEWAYS = "gateways"
-CONF_NODES = "nodes"
-CONF_PERSISTENCE = "persistence"
-CONF_PERSISTENCE_FILE = "persistence_file"
-CONF_RETAIN = "retain"
-CONF_TCP_PORT = "tcp_port"
-CONF_TOPIC_IN_PREFIX = "topic_in_prefix"
-CONF_TOPIC_OUT_PREFIX = "topic_out_prefix"
-CONF_VERSION = "version"
-
-DOMAIN = "mysensors"
-MYSENSORS_GATEWAY_READY = "mysensors_gateway_ready_{}"
-MYSENSORS_GATEWAYS = "mysensors_gateways"
-PLATFORM = "platform"
-SCHEMA = "schema"
-CHILD_CALLBACK = "mysensors_child_callback_{}_{}_{}_{}"
-NODE_CALLBACK = "mysensors_node_callback_{}_{}"
-TYPE = "type"
-UPDATE_DELAY = 0.1
-
-SERVICE_SEND_IR_CODE = "send_ir_code"
-
-BINARY_SENSOR_TYPES = {
+from typing import Dict, List, Literal, Set, Tuple
+
+ATTR_DEVICES: str = "devices"
+ATTR_GATEWAY_ID: str = "gateway_id"
+
+CONF_BAUD_RATE: str = "baud_rate"
+CONF_DEVICE: str = "device"
+CONF_GATEWAYS: str = "gateways"
+CONF_NODES: str = "nodes"
+CONF_PERSISTENCE: str = "persistence"
+CONF_PERSISTENCE_FILE: str = "persistence_file"
+CONF_RETAIN: str = "retain"
+CONF_TCP_PORT: str = "tcp_port"
+CONF_TOPIC_IN_PREFIX: str = "topic_in_prefix"
+CONF_TOPIC_OUT_PREFIX: str = "topic_out_prefix"
+CONF_VERSION: str = "version"
+CONF_GATEWAY_TYPE: str = "gateway_type"
+ConfGatewayType = Literal["Serial", "TCP", "MQTT"]
+CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial"
+CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP"
+CONF_GATEWAY_TYPE_MQTT: ConfGatewayType = "MQTT"
+CONF_GATEWAY_TYPE_ALL: List[str] = [
+    CONF_GATEWAY_TYPE_MQTT,
+    CONF_GATEWAY_TYPE_SERIAL,
+    CONF_GATEWAY_TYPE_TCP,
+]
+
+
+DOMAIN: str = "mysensors"
+MYSENSORS_GATEWAY_READY: str = "mysensors_gateway_ready_{}"
+MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}"
+MYSENSORS_GATEWAYS: str = "mysensors_gateways"
+PLATFORM: str = "platform"
+SCHEMA: str = "schema"
+CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}"
+NODE_CALLBACK: str = "mysensors_node_callback_{}_{}"
+MYSENSORS_DISCOVERY = "mysensors_discovery_{}_{}"
+MYSENSORS_ON_UNLOAD = "mysensors_on_unload_{}"
+TYPE: str = "type"
+UPDATE_DELAY: float = 0.1
+
+SERVICE_SEND_IR_CODE: str = "send_ir_code"
+
+SensorType = str
+# S_DOOR, S_MOTION, S_SMOKE, ...
+
+ValueType = str
+# V_TRIPPED, V_ARMED, V_STATUS, V_PERCENTAGE, ...
+
+GatewayId = str
+# a unique id generated by config_flow.py and stored in the ConfigEntry as the entry id.
+#
+# Gateway may be fetched by giving the gateway id to get_mysensors_gateway()
+
+DevId = Tuple[GatewayId, int, int, int]
+# describes the backend of a hass entity. Contents are: GatewayId, node_id, child_id, v_type as int
+#
+# The string version of v_type can be looked up in the enum gateway.const.SetReq of the appropriate BaseAsyncGateway
+# Home Assistant Entities are quite limited and only ever do one thing.
+# MySensors Nodes have multiple child_ids each with a s_type several associated v_types
+# The MySensors integration brings these together by creating an entity for every v_type of every child_id of every node.
+# The DevId tuple perfectly captures this.
+
+BINARY_SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = {
     "S_DOOR": {"V_TRIPPED"},
     "S_MOTION": {"V_TRIPPED"},
     "S_SMOKE": {"V_TRIPPED"},
@@ -38,21 +74,23 @@ BINARY_SENSOR_TYPES = {
     "S_MOISTURE": {"V_TRIPPED"},
 }
 
-CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}}
+CLIMATE_TYPES: Dict[SensorType, Set[ValueType]] = {"S_HVAC": {"V_HVAC_FLOW_STATE"}}
 
-COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}}
+COVER_TYPES: Dict[SensorType, Set[ValueType]] = {
+    "S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}
+}
 
-DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}}
+DEVICE_TRACKER_TYPES: Dict[SensorType, Set[ValueType]] = {"S_GPS": {"V_POSITION"}}
 
-LIGHT_TYPES = {
+LIGHT_TYPES: Dict[SensorType, Set[ValueType]] = {
     "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"},
     "S_RGB_LIGHT": {"V_RGB"},
     "S_RGBW_LIGHT": {"V_RGBW"},
 }
 
-NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}}
+NOTIFY_TYPES: Dict[SensorType, Set[ValueType]] = {"S_INFO": {"V_TEXT"}}
 
-SENSOR_TYPES = {
+SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = {
     "S_SOUND": {"V_LEVEL"},
     "S_VIBRATION": {"V_LEVEL"},
     "S_MOISTURE": {"V_LEVEL"},
@@ -80,7 +118,7 @@ SENSOR_TYPES = {
     "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"},
 }
 
-SWITCH_TYPES = {
+SWITCH_TYPES: Dict[SensorType, Set[ValueType]] = {
     "S_LIGHT": {"V_LIGHT"},
     "S_BINARY": {"V_STATUS"},
     "S_DOOR": {"V_ARMED"},
@@ -97,7 +135,7 @@ SWITCH_TYPES = {
 }
 
 
-PLATFORM_TYPES = {
+PLATFORM_TYPES: Dict[str, Dict[SensorType, Set[ValueType]]] = {
     "binary_sensor": BINARY_SENSOR_TYPES,
     "climate": CLIMATE_TYPES,
     "cover": COVER_TYPES,
@@ -108,13 +146,19 @@ PLATFORM_TYPES = {
     "switch": SWITCH_TYPES,
 }
 
-FLAT_PLATFORM_TYPES = {
+FLAT_PLATFORM_TYPES: Dict[Tuple[str, SensorType], Set[ValueType]] = {
     (platform, s_type_name): v_type_name
     for platform, platform_types in PLATFORM_TYPES.items()
     for s_type_name, v_type_name in platform_types.items()
 }
 
-TYPE_TO_PLATFORMS = defaultdict(list)
+TYPE_TO_PLATFORMS: Dict[SensorType, List[str]] = defaultdict(list)
+
 for platform, platform_types in PLATFORM_TYPES.items():
     for s_type_name in platform_types:
         TYPE_TO_PLATFORMS[s_type_name].append(platform)
+
+SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - {
+    "notify",
+    "device_tracker",
+}
diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py
index f2ede69793fde0e4c713f6eead4624c5659a397f..782ab88c48899e069e776740ceabc06cf3dc0653 100644
--- a/homeassistant/components/mysensors/cover.py
+++ b/homeassistant/components/mysensors/cover.py
@@ -1,28 +1,48 @@
 """Support for MySensors covers."""
+import logging
+from typing import Callable
+
 from homeassistant.components import mysensors
 from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
+
+_LOGGER = logging.getLogger(__name__)
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the mysensors platform for covers."""
-    mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+    hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+    """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+    async def async_discover(discovery_info):
+        """Discover and add a MySensors cover."""
+        mysensors.setup_mysensors_platform(
+            hass,
+            DOMAIN,
+            discovery_info,
+            MySensorsCover,
+            async_add_entities=async_add_entities,
+        )
+
+    await on_unload(
         hass,
-        DOMAIN,
-        discovery_info,
-        MySensorsCover,
-        async_add_entities=async_add_entities,
+        config_entry.entry_id,
+        async_dispatcher_connect(
+            hass,
+            MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+            async_discover,
+        ),
     )
 
 
 class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
     """Representation of the value of a MySensors Cover child node."""
 
-    @property
-    def assumed_state(self):
-        """Return True if unable to access real state of entity."""
-        return self.gateway.optimistic
-
     @property
     def is_closed(self):
         """Return True if cover is closed."""
@@ -46,7 +66,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
         self.gateway.set_child_value(
             self.node_id, self.child_id, set_req.V_UP, 1, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that cover has changed state.
             if set_req.V_DIMMER in self._values:
                 self._values[set_req.V_DIMMER] = 100
@@ -60,7 +80,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
         self.gateway.set_child_value(
             self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that cover has changed state.
             if set_req.V_DIMMER in self._values:
                 self._values[set_req.V_DIMMER] = 0
@@ -75,7 +95,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
         self.gateway.set_child_value(
             self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that cover has changed state.
             self._values[set_req.V_DIMMER] = position
             self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py
index 9c1c4b543676466f46434cfee98d2a43b5e087b1..6841486734520ff41a3292d9fefd0290c7868eef 100644
--- a/homeassistant/components/mysensors/device.py
+++ b/homeassistant/components/mysensors/device.py
@@ -1,13 +1,26 @@
 """Handle MySensors devices."""
 from functools import partial
 import logging
+from typing import Any, Dict, Optional
+
+from mysensors import BaseAsyncGateway, Sensor
+from mysensors.sensor import ChildSensor
 
 from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
 from homeassistant.core import callback
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity import Entity
 
-from .const import CHILD_CALLBACK, NODE_CALLBACK, UPDATE_DELAY
+from .const import (
+    CHILD_CALLBACK,
+    CONF_DEVICE,
+    DOMAIN,
+    NODE_CALLBACK,
+    PLATFORM_TYPES,
+    UPDATE_DELAY,
+    DevId,
+    GatewayId,
+)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -19,33 +32,94 @@ ATTR_HEARTBEAT = "heartbeat"
 MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}"
 
 
-def get_mysensors_devices(hass, domain):
-    """Return MySensors devices for a platform."""
-    if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data:
-        hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
-    return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)]
-
-
 class MySensorsDevice:
     """Representation of a MySensors device."""
 
-    def __init__(self, gateway, node_id, child_id, name, value_type):
+    def __init__(
+        self,
+        gateway_id: GatewayId,
+        gateway: BaseAsyncGateway,
+        node_id: int,
+        child_id: int,
+        value_type: int,
+    ):
         """Set up the MySensors device."""
-        self.gateway = gateway
-        self.node_id = node_id
-        self.child_id = child_id
-        self._name = name
-        self.value_type = value_type
-        child = gateway.sensors[node_id].children[child_id]
-        self.child_type = child.type
+        self.gateway_id: GatewayId = gateway_id
+        self.gateway: BaseAsyncGateway = gateway
+        self.node_id: int = node_id
+        self.child_id: int = child_id
+        self.value_type: int = value_type  # value_type as int. string variant can be looked up in gateway consts
+        self.child_type = self._child.type
         self._values = {}
         self._update_scheduled = False
         self.hass = None
 
+    @property
+    def dev_id(self) -> DevId:
+        """Return the DevId of this device.
+
+        It is used to route incoming MySensors messages to the correct device/entity.
+        """
+        return self.gateway_id, self.node_id, self.child_id, self.value_type
+
+    @property
+    def _logger(self):
+        return logging.getLogger(f"{__name__}.{self.name}")
+
+    async def async_will_remove_from_hass(self):
+        """Remove this entity from home assistant."""
+        for platform in PLATFORM_TYPES:
+            platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform)
+            if platform_str in self.hass.data[DOMAIN]:
+                platform_dict = self.hass.data[DOMAIN][platform_str]
+                if self.dev_id in platform_dict:
+                    del platform_dict[self.dev_id]
+                    self._logger.debug(
+                        "deleted %s from platform %s", self.dev_id, platform
+                    )
+
+    @property
+    def _node(self) -> Sensor:
+        return self.gateway.sensors[self.node_id]
+
+    @property
+    def _child(self) -> ChildSensor:
+        return self._node.children[self.child_id]
+
+    @property
+    def sketch_name(self) -> str:
+        """Return the name of the sketch running on the whole node (will be the same for several entities!)."""
+        return self._node.sketch_name
+
+    @property
+    def sketch_version(self) -> str:
+        """Return the version of the sketch running on the whole node (will be the same for several entities!)."""
+        return self._node.sketch_version
+
+    @property
+    def node_name(self) -> str:
+        """Name of the whole node (will be the same for several entities!)."""
+        return f"{self.sketch_name} {self.node_id}"
+
+    @property
+    def unique_id(self) -> str:
+        """Return a unique ID for use in home assistant."""
+        return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}"
+
+    @property
+    def device_info(self) -> Optional[Dict[str, Any]]:
+        """Return a dict that allows home assistant to puzzle all entities belonging to a node together."""
+        return {
+            "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")},
+            "name": self.node_name,
+            "manufacturer": DOMAIN,
+            "sw_version": self.sketch_version,
+        }
+
     @property
     def name(self):
         """Return the name of this entity."""
-        return self._name
+        return f"{self.node_name} {self.child_id}"
 
     @property
     def device_state_attributes(self):
@@ -57,9 +131,12 @@ class MySensorsDevice:
             ATTR_HEARTBEAT: node.heartbeat,
             ATTR_CHILD_ID: self.child_id,
             ATTR_DESCRIPTION: child.description,
-            ATTR_DEVICE: self.gateway.device,
             ATTR_NODE_ID: self.node_id,
         }
+        # This works when we are actually an Entity (i.e. all platforms except device_tracker)
+        if hasattr(self, "platform"):
+            # pylint: disable=no-member
+            attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
 
         set_req = self.gateway.const.SetReq
 
@@ -76,7 +153,7 @@ class MySensorsDevice:
         for value_type, value in child.values.items():
             _LOGGER.debug(
                 "Entity update: %s: value_type %s, value = %s",
-                self._name,
+                self.name,
                 value_type,
                 value,
             )
@@ -116,6 +193,13 @@ class MySensorsDevice:
         self.hass.loop.call_later(UPDATE_DELAY, delayed_update)
 
 
+def get_mysensors_devices(hass, domain: str) -> Dict[DevId, MySensorsDevice]:
+    """Return MySensors devices for a hass platform name."""
+    if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]:
+        hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
+    return hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)]
+
+
 class MySensorsEntity(MySensorsDevice, Entity):
     """Representation of a MySensors entity."""
 
@@ -135,17 +219,17 @@ class MySensorsEntity(MySensorsDevice, Entity):
 
     async def async_added_to_hass(self):
         """Register update callback."""
-        gateway_id = id(self.gateway)
-        dev_id = gateway_id, self.node_id, self.child_id, self.value_type
         self.async_on_remove(
             async_dispatcher_connect(
-                self.hass, CHILD_CALLBACK.format(*dev_id), self.async_update_callback
+                self.hass,
+                CHILD_CALLBACK.format(*self.dev_id),
+                self.async_update_callback,
             )
         )
         self.async_on_remove(
             async_dispatcher_connect(
                 self.hass,
-                NODE_CALLBACK.format(gateway_id, self.node_id),
+                NODE_CALLBACK.format(self.gateway_id, self.node_id),
                 self.async_update_callback,
             )
         )
diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py
index 1bf1e072cebfd24b83fc9bd233eed43251b5efed..b395a48f28b7dad40de8830466c0a310e312542a 100644
--- a/homeassistant/components/mysensors/device_tracker.py
+++ b/homeassistant/components/mysensors/device_tracker.py
@@ -1,11 +1,16 @@
 """Support for tracking MySensors devices."""
 from homeassistant.components import mysensors
 from homeassistant.components.device_tracker import DOMAIN
+from homeassistant.components.mysensors import DevId, on_unload
+from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
 from homeassistant.util import slugify
 
 
-async def async_setup_scanner(hass, config, async_see, discovery_info=None):
+async def async_setup_scanner(
+    hass: HomeAssistantType, config, async_see, discovery_info=None
+):
     """Set up the MySensors device scanner."""
     new_devices = mysensors.setup_mysensors_platform(
         hass,
@@ -18,17 +23,25 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
         return False
 
     for device in new_devices:
-        gateway_id = id(device.gateway)
-        dev_id = (gateway_id, device.node_id, device.child_id, device.value_type)
-        async_dispatcher_connect(
+        gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID]
+        dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type)
+        await on_unload(
             hass,
-            mysensors.const.CHILD_CALLBACK.format(*dev_id),
-            device.async_update_callback,
+            gateway_id,
+            async_dispatcher_connect(
+                hass,
+                mysensors.const.CHILD_CALLBACK.format(*dev_id),
+                device.async_update_callback,
+            ),
         )
-        async_dispatcher_connect(
+        await on_unload(
             hass,
-            mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id),
-            device.async_update_callback,
+            gateway_id,
+            async_dispatcher_connect(
+                hass,
+                mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id),
+                device.async_update_callback,
+            ),
         )
 
     return True
@@ -37,7 +50,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
 class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
     """Represent a MySensors scanner."""
 
-    def __init__(self, hass, async_see, *args):
+    def __init__(self, hass: HomeAssistantType, async_see, *args):
         """Set up instance."""
         super().__init__(*args)
         self.async_see = async_see
diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py
index f9450b798ace1eaa0c4221d679c00144e46eaeb6..b618004b62222d49808a6ea855a8678cfe1e521b 100644
--- a/homeassistant/components/mysensors/gateway.py
+++ b/homeassistant/components/mysensors/gateway.py
@@ -4,22 +4,21 @@ from collections import defaultdict
 import logging
 import socket
 import sys
+from typing import Any, Callable, Coroutine, Dict, Optional
 
 import async_timeout
-from mysensors import mysensors
+from mysensors import BaseAsyncGateway, Message, Sensor, mysensors
 import voluptuous as vol
 
-from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import callback
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import Event, callback
 import homeassistant.helpers.config_validation as cv
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers.typing import HomeAssistantType
 
 from .const import (
     CONF_BAUD_RATE,
     CONF_DEVICE,
-    CONF_GATEWAYS,
-    CONF_NODES,
-    CONF_PERSISTENCE,
     CONF_PERSISTENCE_FILE,
     CONF_RETAIN,
     CONF_TCP_PORT,
@@ -28,7 +27,9 @@ from .const import (
     CONF_VERSION,
     DOMAIN,
     MYSENSORS_GATEWAY_READY,
+    MYSENSORS_GATEWAY_START_TASK,
     MYSENSORS_GATEWAYS,
+    GatewayId,
 )
 from .handler import HANDLERS
 from .helpers import discover_mysensors_platform, validate_child, validate_node
@@ -58,48 +59,114 @@ def is_socket_address(value):
         raise vol.Invalid("Device is not a valid domain name or ip address") from err
 
 
-def get_mysensors_gateway(hass, gateway_id):
-    """Return MySensors gateway."""
-    if MYSENSORS_GATEWAYS not in hass.data:
-        hass.data[MYSENSORS_GATEWAYS] = {}
-    gateways = hass.data.get(MYSENSORS_GATEWAYS)
-    return gateways.get(gateway_id)
-
+async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bool:
+    """Try to connect to a gateway and report if it worked."""
+    if user_input[CONF_DEVICE] == MQTT_COMPONENT:
+        return True  # dont validate mqtt. mqtt gateways dont send ready messages :(
+    try:
+        gateway_ready = asyncio.Future()
+
+        def gateway_ready_callback(msg):
+            msg_type = msg.gateway.const.MessageType(msg.type)
+            _LOGGER.debug("Received MySensors msg type %s: %s", msg_type.name, msg)
+            if msg_type.name != "internal":
+                return
+            internal = msg.gateway.const.Internal(msg.sub_type)
+            if internal.name != "I_GATEWAY_READY":
+                return
+            _LOGGER.debug("Received gateway ready")
+            gateway_ready.set_result(True)
+
+        gateway: Optional[BaseAsyncGateway] = await _get_gateway(
+            hass,
+            device=user_input[CONF_DEVICE],
+            version=user_input[CONF_VERSION],
+            event_callback=gateway_ready_callback,
+            persistence_file=None,
+            baud_rate=user_input.get(CONF_BAUD_RATE),
+            tcp_port=user_input.get(CONF_TCP_PORT),
+            topic_in_prefix=None,
+            topic_out_prefix=None,
+            retain=False,
+            persistence=False,
+        )
+        if gateway is None:
+            return False
 
-async def setup_gateways(hass, config):
-    """Set up all gateways."""
-    conf = config[DOMAIN]
-    gateways = {}
+        connect_task = None
+        try:
+            connect_task = asyncio.create_task(gateway.start())
+            with async_timeout.timeout(5):
+                await gateway_ready
+                return True
+        except asyncio.TimeoutError:
+            _LOGGER.info("Try gateway connect failed with timeout")
+            return False
+        finally:
+            if connect_task is not None and not connect_task.done():
+                connect_task.cancel()
+            asyncio.create_task(gateway.stop())
+    except OSError as err:
+        _LOGGER.info("Try gateway connect failed with exception", exc_info=err)
+        return False
 
-    for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]):
-        persistence_file = gateway_conf.get(
-            CONF_PERSISTENCE_FILE,
-            hass.config.path(f"mysensors{index + 1}.pickle"),
-        )
-        ready_gateway = await _get_gateway(hass, config, gateway_conf, persistence_file)
-        if ready_gateway is not None:
-            gateways[id(ready_gateway)] = ready_gateway
 
-    return gateways
+def get_mysensors_gateway(
+    hass: HomeAssistantType, gateway_id: GatewayId
+) -> Optional[BaseAsyncGateway]:
+    """Return the Gateway for a given GatewayId."""
+    if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]:
+        hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {}
+    gateways = hass.data[DOMAIN].get(MYSENSORS_GATEWAYS)
+    return gateways.get(gateway_id)
 
 
-async def _get_gateway(hass, config, gateway_conf, persistence_file):
+async def setup_gateway(
+    hass: HomeAssistantType, entry: ConfigEntry
+) -> Optional[BaseAsyncGateway]:
+    """Set up the Gateway for the given ConfigEntry."""
+
+    ready_gateway = await _get_gateway(
+        hass,
+        device=entry.data[CONF_DEVICE],
+        version=entry.data[CONF_VERSION],
+        event_callback=_gw_callback_factory(hass, entry.entry_id),
+        persistence_file=entry.data.get(
+            CONF_PERSISTENCE_FILE, f"mysensors_{entry.entry_id}.json"
+        ),
+        baud_rate=entry.data.get(CONF_BAUD_RATE),
+        tcp_port=entry.data.get(CONF_TCP_PORT),
+        topic_in_prefix=entry.data.get(CONF_TOPIC_IN_PREFIX),
+        topic_out_prefix=entry.data.get(CONF_TOPIC_OUT_PREFIX),
+        retain=entry.data.get(CONF_RETAIN, False),
+    )
+    return ready_gateway
+
+
+async def _get_gateway(
+    hass: HomeAssistantType,
+    device: str,
+    version: str,
+    event_callback: Callable[[Message], None],
+    persistence_file: Optional[str] = None,
+    baud_rate: Optional[int] = None,
+    tcp_port: Optional[int] = None,
+    topic_in_prefix: Optional[str] = None,
+    topic_out_prefix: Optional[str] = None,
+    retain: bool = False,
+    persistence: bool = True,  # old persistence option has been deprecated. kwarg is here so we can run try_connect() without persistence
+) -> Optional[BaseAsyncGateway]:
     """Return gateway after setup of the gateway."""
 
-    conf = config[DOMAIN]
-    persistence = conf[CONF_PERSISTENCE]
-    version = conf[CONF_VERSION]
-    device = gateway_conf[CONF_DEVICE]
-    baud_rate = gateway_conf[CONF_BAUD_RATE]
-    tcp_port = gateway_conf[CONF_TCP_PORT]
-    in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, "")
-    out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, "")
+    if persistence_file is not None:
+        # interpret relative paths to be in hass config folder. absolute paths will be left as they are
+        persistence_file = hass.config.path(persistence_file)
 
     if device == MQTT_COMPONENT:
-        if not await async_setup_component(hass, MQTT_COMPONENT, config):
-            return None
+        # what is the purpose of this?
+        # if not await async_setup_component(hass, MQTT_COMPONENT, entry):
+        #    return None
         mqtt = hass.components.mqtt
-        retain = conf[CONF_RETAIN]
 
         def pub_callback(topic, payload, qos, retain):
             """Call MQTT publish function."""
@@ -118,8 +185,8 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file):
         gateway = mysensors.AsyncMQTTGateway(
             pub_callback,
             sub_callback,
-            in_prefix=in_prefix,
-            out_prefix=out_prefix,
+            in_prefix=topic_in_prefix,
+            out_prefix=topic_out_prefix,
             retain=retain,
             loop=hass.loop,
             event_callback=None,
@@ -154,25 +221,23 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file):
                 )
             except vol.Invalid:
                 # invalid ip address
+                _LOGGER.error("Connect failed: Invalid device %s", device)
                 return None
-    gateway.metric = hass.config.units.is_metric
-    gateway.optimistic = conf[CONF_OPTIMISTIC]
-    gateway.device = device
-    gateway.event_callback = _gw_callback_factory(hass, config)
-    gateway.nodes_config = gateway_conf[CONF_NODES]
+    gateway.event_callback = event_callback
     if persistence:
         await gateway.start_persistence()
 
     return gateway
 
 
-async def finish_setup(hass, hass_config, gateways):
+async def finish_setup(
+    hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
+):
     """Load any persistent devices and platforms and start gateway."""
     discover_tasks = []
     start_tasks = []
-    for gateway in gateways.values():
-        discover_tasks.append(_discover_persistent_devices(hass, hass_config, gateway))
-        start_tasks.append(_gw_start(hass, gateway))
+    discover_tasks.append(_discover_persistent_devices(hass, entry, gateway))
+    start_tasks.append(_gw_start(hass, entry, gateway))
     if discover_tasks:
         # Make sure all devices and platforms are loaded before gateway start.
         await asyncio.wait(discover_tasks)
@@ -180,43 +245,58 @@ async def finish_setup(hass, hass_config, gateways):
         await asyncio.wait(start_tasks)
 
 
-async def _discover_persistent_devices(hass, hass_config, gateway):
+async def _discover_persistent_devices(
+    hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
+):
     """Discover platforms for devices loaded via persistence file."""
     tasks = []
     new_devices = defaultdict(list)
     for node_id in gateway.sensors:
         if not validate_node(gateway, node_id):
             continue
-        node = gateway.sensors[node_id]
-        for child in node.children.values():
-            validated = validate_child(gateway, node_id, child)
+        node: Sensor = gateway.sensors[node_id]
+        for child in node.children.values():  # child is of type ChildSensor
+            validated = validate_child(entry.entry_id, gateway, node_id, child)
             for platform, dev_ids in validated.items():
                 new_devices[platform].extend(dev_ids)
+    _LOGGER.debug("discovering persistent devices: %s", new_devices)
     for platform, dev_ids in new_devices.items():
-        tasks.append(discover_mysensors_platform(hass, hass_config, platform, dev_ids))
+        discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids)
     if tasks:
         await asyncio.wait(tasks)
 
 
-async def _gw_start(hass, gateway):
+async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway):
+    """Stop the gateway."""
+    connect_task = hass.data[DOMAIN].get(
+        MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id)
+    )
+    if connect_task is not None and not connect_task.done():
+        connect_task.cancel()
+    await gateway.stop()
+
+
+async def _gw_start(
+    hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway
+):
     """Start the gateway."""
     # Don't use hass.async_create_task to avoid holding up setup indefinitely.
-    connect_task = hass.loop.create_task(gateway.start())
+    hass.data[DOMAIN][
+        MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id)
+    ] = asyncio.create_task(
+        gateway.start()
+    )  # store the connect task so it can be cancelled in gw_stop
 
-    @callback
-    def gw_stop(event):
-        """Trigger to stop the gateway."""
-        hass.async_create_task(gateway.stop())
-        if not connect_task.done():
-            connect_task.cancel()
-
-    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop)
-    if gateway.device == "mqtt":
+    async def stop_this_gw(_: Event):
+        await gw_stop(hass, entry, gateway)
+
+    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw)
+    if entry.data[CONF_DEVICE] == MQTT_COMPONENT:
         # Gatways connected via mqtt doesn't send gateway ready message.
         return
     gateway_ready = asyncio.Future()
-    gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway))
-    hass.data[gateway_ready_key] = gateway_ready
+    gateway_ready_key = MYSENSORS_GATEWAY_READY.format(entry.entry_id)
+    hass.data[DOMAIN][gateway_ready_key] = gateway_ready
 
     try:
         with async_timeout.timeout(GATEWAY_READY_TIMEOUT):
@@ -224,27 +304,35 @@ async def _gw_start(hass, gateway):
     except asyncio.TimeoutError:
         _LOGGER.warning(
             "Gateway %s not ready after %s secs so continuing with setup",
-            gateway.device,
+            entry.data[CONF_DEVICE],
             GATEWAY_READY_TIMEOUT,
         )
     finally:
-        hass.data.pop(gateway_ready_key, None)
+        hass.data[DOMAIN].pop(gateway_ready_key, None)
 
 
-def _gw_callback_factory(hass, hass_config):
+def _gw_callback_factory(
+    hass: HomeAssistantType, gateway_id: GatewayId
+) -> Callable[[Message], None]:
     """Return a new callback for the gateway."""
 
     @callback
-    def mysensors_callback(msg):
-        """Handle messages from a MySensors gateway."""
+    def mysensors_callback(msg: Message):
+        """Handle messages from a MySensors gateway.
+
+        All MySenors messages are received here.
+        The messages are passed to handler functions depending on their type.
+        """
         _LOGGER.debug("Node update: node %s child %s", msg.node_id, msg.child_id)
 
         msg_type = msg.gateway.const.MessageType(msg.type)
-        msg_handler = HANDLERS.get(msg_type.name)
+        msg_handler: Callable[
+            [Any, GatewayId, Message], Coroutine[None]
+        ] = HANDLERS.get(msg_type.name)
 
         if msg_handler is None:
             return
 
-        hass.async_create_task(msg_handler(hass, hass_config, msg))
+        hass.async_create_task(msg_handler(hass, gateway_id, msg))
 
     return mysensors_callback
diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py
index b5b8b511aee11060ce80f038652337df71dce110..10165a171e0a035817c32aff991cb4b305c33d3f 100644
--- a/homeassistant/components/mysensors/handler.py
+++ b/homeassistant/components/mysensors/handler.py
@@ -1,9 +1,21 @@
 """Handle MySensors messages."""
+from typing import Dict, List
+
+from mysensors import Message
+
 from homeassistant.core import callback
 from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.typing import HomeAssistantType
 from homeassistant.util import decorator
 
-from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK
+from .const import (
+    CHILD_CALLBACK,
+    DOMAIN,
+    MYSENSORS_GATEWAY_READY,
+    NODE_CALLBACK,
+    DevId,
+    GatewayId,
+)
 from .device import get_mysensors_devices
 from .helpers import discover_mysensors_platform, validate_set_msg
 
@@ -11,75 +23,91 @@ HANDLERS = decorator.Registry()
 
 
 @HANDLERS.register("set")
-async def handle_set(hass, hass_config, msg):
+async def handle_set(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle a mysensors set message."""
-    validated = validate_set_msg(msg)
-    _handle_child_update(hass, hass_config, validated)
+    validated = validate_set_msg(gateway_id, msg)
+    _handle_child_update(hass, gateway_id, validated)
 
 
 @HANDLERS.register("internal")
-async def handle_internal(hass, hass_config, msg):
+async def handle_internal(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle a mysensors internal message."""
     internal = msg.gateway.const.Internal(msg.sub_type)
     handler = HANDLERS.get(internal.name)
     if handler is None:
         return
-    await handler(hass, hass_config, msg)
+    await handler(hass, gateway_id, msg)
 
 
 @HANDLERS.register("I_BATTERY_LEVEL")
-async def handle_battery_level(hass, hass_config, msg):
+async def handle_battery_level(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle an internal battery level message."""
-    _handle_node_update(hass, msg)
+    _handle_node_update(hass, gateway_id, msg)
 
 
 @HANDLERS.register("I_HEARTBEAT_RESPONSE")
-async def handle_heartbeat(hass, hass_config, msg):
+async def handle_heartbeat(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle an heartbeat."""
-    _handle_node_update(hass, msg)
+    _handle_node_update(hass, gateway_id, msg)
 
 
 @HANDLERS.register("I_SKETCH_NAME")
-async def handle_sketch_name(hass, hass_config, msg):
+async def handle_sketch_name(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle an internal sketch name message."""
-    _handle_node_update(hass, msg)
+    _handle_node_update(hass, gateway_id, msg)
 
 
 @HANDLERS.register("I_SKETCH_VERSION")
-async def handle_sketch_version(hass, hass_config, msg):
+async def handle_sketch_version(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle an internal sketch version message."""
-    _handle_node_update(hass, msg)
+    _handle_node_update(hass, gateway_id, msg)
 
 
 @HANDLERS.register("I_GATEWAY_READY")
-async def handle_gateway_ready(hass, hass_config, msg):
+async def handle_gateway_ready(
+    hass: HomeAssistantType, gateway_id: GatewayId, msg: Message
+) -> None:
     """Handle an internal gateway ready message.
 
     Set asyncio future result if gateway is ready.
     """
-    gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(id(msg.gateway)))
+    gateway_ready = hass.data[DOMAIN].get(MYSENSORS_GATEWAY_READY.format(gateway_id))
     if gateway_ready is None or gateway_ready.cancelled():
         return
     gateway_ready.set_result(True)
 
 
 @callback
-def _handle_child_update(hass, hass_config, validated):
+def _handle_child_update(
+    hass: HomeAssistantType, gateway_id: GatewayId, validated: Dict[str, List[DevId]]
+):
     """Handle a child update."""
-    signals = []
+    signals: List[str] = []
 
     # Update all platforms for the device via dispatcher.
     # Add/update entity for validated children.
     for platform, dev_ids in validated.items():
         devices = get_mysensors_devices(hass, platform)
-        new_dev_ids = []
+        new_dev_ids: List[DevId] = []
         for dev_id in dev_ids:
             if dev_id in devices:
                 signals.append(CHILD_CALLBACK.format(*dev_id))
             else:
                 new_dev_ids.append(dev_id)
         if new_dev_ids:
-            discover_mysensors_platform(hass, hass_config, platform, new_dev_ids)
+            discover_mysensors_platform(hass, gateway_id, platform, new_dev_ids)
     for signal in set(signals):
         # Only one signal per device is needed.
         # A device can have multiple platforms, ie multiple schemas.
@@ -87,7 +115,7 @@ def _handle_child_update(hass, hass_config, validated):
 
 
 @callback
-def _handle_node_update(hass, msg):
+def _handle_node_update(hass: HomeAssistantType, gateway_id: GatewayId, msg: Message):
     """Handle a node update."""
-    signal = NODE_CALLBACK.format(id(msg.gateway), msg.node_id)
+    signal = NODE_CALLBACK.format(gateway_id, msg.node_id)
     async_dispatcher_send(hass, signal)
diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py
index 20b266e550e61955606873907024ded5ed23cf68..d06bf0dee2fe5764d0e6a4c44d4b6c60d07fbbce 100644
--- a/homeassistant/components/mysensors/helpers.py
+++ b/homeassistant/components/mysensors/helpers.py
@@ -1,78 +1,109 @@
 """Helper functions for mysensors package."""
 from collections import defaultdict
+from enum import IntEnum
 import logging
+from typing import DefaultDict, Dict, List, Optional, Set
 
+from mysensors import BaseAsyncGateway, Message
+from mysensors.sensor import ChildSensor
 import voluptuous as vol
 
 from homeassistant.const import CONF_NAME
 from homeassistant.core import callback
-from homeassistant.helpers import discovery
 import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_send
 from homeassistant.util.decorator import Registry
 
-from .const import ATTR_DEVICES, DOMAIN, FLAT_PLATFORM_TYPES, TYPE_TO_PLATFORMS
+from .const import (
+    ATTR_DEVICES,
+    ATTR_GATEWAY_ID,
+    DOMAIN,
+    FLAT_PLATFORM_TYPES,
+    MYSENSORS_DISCOVERY,
+    TYPE_TO_PLATFORMS,
+    DevId,
+    GatewayId,
+    SensorType,
+    ValueType,
+)
 
 _LOGGER = logging.getLogger(__name__)
 SCHEMAS = Registry()
 
 
 @callback
-def discover_mysensors_platform(hass, hass_config, platform, new_devices):
+def discover_mysensors_platform(
+    hass, gateway_id: GatewayId, platform: str, new_devices: List[DevId]
+) -> None:
     """Discover a MySensors platform."""
-    task = hass.async_create_task(
-        discovery.async_load_platform(
-            hass,
-            platform,
-            DOMAIN,
-            {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN},
-            hass_config,
-        )
+    _LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices)
+    async_dispatcher_send(
+        hass,
+        MYSENSORS_DISCOVERY.format(gateway_id, platform),
+        {
+            ATTR_DEVICES: new_devices,
+            CONF_NAME: DOMAIN,
+            ATTR_GATEWAY_ID: gateway_id,
+        },
     )
-    return task
 
 
-def default_schema(gateway, child, value_type_name):
+def default_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
     """Return a default validation schema for value types."""
     schema = {value_type_name: cv.string}
     return get_child_schema(gateway, child, value_type_name, schema)
 
 
 @SCHEMAS.register(("light", "V_DIMMER"))
-def light_dimmer_schema(gateway, child, value_type_name):
+def light_dimmer_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
     """Return a validation schema for V_DIMMER."""
     schema = {"V_DIMMER": cv.string, "V_LIGHT": cv.string}
     return get_child_schema(gateway, child, value_type_name, schema)
 
 
 @SCHEMAS.register(("light", "V_PERCENTAGE"))
-def light_percentage_schema(gateway, child, value_type_name):
+def light_percentage_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
     """Return a validation schema for V_PERCENTAGE."""
     schema = {"V_PERCENTAGE": cv.string, "V_STATUS": cv.string}
     return get_child_schema(gateway, child, value_type_name, schema)
 
 
 @SCHEMAS.register(("light", "V_RGB"))
-def light_rgb_schema(gateway, child, value_type_name):
+def light_rgb_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
     """Return a validation schema for V_RGB."""
     schema = {"V_RGB": cv.string, "V_STATUS": cv.string}
     return get_child_schema(gateway, child, value_type_name, schema)
 
 
 @SCHEMAS.register(("light", "V_RGBW"))
-def light_rgbw_schema(gateway, child, value_type_name):
+def light_rgbw_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
     """Return a validation schema for V_RGBW."""
     schema = {"V_RGBW": cv.string, "V_STATUS": cv.string}
     return get_child_schema(gateway, child, value_type_name, schema)
 
 
 @SCHEMAS.register(("switch", "V_IR_SEND"))
-def switch_ir_send_schema(gateway, child, value_type_name):
+def switch_ir_send_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+) -> vol.Schema:
     """Return a validation schema for V_IR_SEND."""
     schema = {"V_IR_SEND": cv.string, "V_LIGHT": cv.string}
     return get_child_schema(gateway, child, value_type_name, schema)
 
 
-def get_child_schema(gateway, child, value_type_name, schema):
+def get_child_schema(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType, schema
+) -> vol.Schema:
     """Return a child schema."""
     set_req = gateway.const.SetReq
     child_schema = child.get_schema(gateway.protocol_version)
@@ -88,7 +119,9 @@ def get_child_schema(gateway, child, value_type_name, schema):
     return schema
 
 
-def invalid_msg(gateway, child, value_type_name):
+def invalid_msg(
+    gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
+):
     """Return a message for an invalid child during schema validation."""
     pres = gateway.const.Presentation
     set_req = gateway.const.SetReq
@@ -97,15 +130,15 @@ def invalid_msg(gateway, child, value_type_name):
     )
 
 
-def validate_set_msg(msg):
+def validate_set_msg(gateway_id: GatewayId, msg: Message) -> Dict[str, List[DevId]]:
     """Validate a set message."""
     if not validate_node(msg.gateway, msg.node_id):
         return {}
     child = msg.gateway.sensors[msg.node_id].children[msg.child_id]
-    return validate_child(msg.gateway, msg.node_id, child, msg.sub_type)
+    return validate_child(gateway_id, msg.gateway, msg.node_id, child, msg.sub_type)
 
 
-def validate_node(gateway, node_id):
+def validate_node(gateway: BaseAsyncGateway, node_id: int) -> bool:
     """Validate a node."""
     if gateway.sensors[node_id].sketch_name is None:
         _LOGGER.debug("Node %s is missing sketch name", node_id)
@@ -113,31 +146,39 @@ def validate_node(gateway, node_id):
     return True
 
 
-def validate_child(gateway, node_id, child, value_type=None):
-    """Validate a child."""
-    validated = defaultdict(list)
-    pres = gateway.const.Presentation
-    set_req = gateway.const.SetReq
-    child_type_name = next(
+def validate_child(
+    gateway_id: GatewayId,
+    gateway: BaseAsyncGateway,
+    node_id: int,
+    child: ChildSensor,
+    value_type: Optional[int] = None,
+) -> DefaultDict[str, List[DevId]]:
+    """Validate a child. Returns a dict mapping hass platform names to list of DevId."""
+    validated: DefaultDict[str, List[DevId]] = defaultdict(list)
+    pres: IntEnum = gateway.const.Presentation
+    set_req: IntEnum = gateway.const.SetReq
+    child_type_name: Optional[SensorType] = next(
         (member.name for member in pres if member.value == child.type), None
     )
-    value_types = {value_type} if value_type else {*child.values}
-    value_type_names = {
+    value_types: Set[int] = {value_type} if value_type else {*child.values}
+    value_type_names: Set[ValueType] = {
         member.name for member in set_req if member.value in value_types
     }
-    platforms = TYPE_TO_PLATFORMS.get(child_type_name, [])
+    platforms: List[str] = TYPE_TO_PLATFORMS.get(child_type_name, [])
     if not platforms:
         _LOGGER.warning("Child type %s is not supported", child.type)
         return validated
 
     for platform in platforms:
-        platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name]
-        v_names = platform_v_names & value_type_names
+        platform_v_names: Set[ValueType] = FLAT_PLATFORM_TYPES[
+            platform, child_type_name
+        ]
+        v_names: Set[ValueType] = platform_v_names & value_type_names
         if not v_names:
-            child_value_names = {
+            child_value_names: Set[ValueType] = {
                 member.name for member in set_req if member.value in child.values
             }
-            v_names = platform_v_names & child_value_names
+            v_names: Set[ValueType] = platform_v_names & child_value_names
 
         for v_name in v_names:
             child_schema_gen = SCHEMAS.get((platform, v_name), default_schema)
@@ -153,7 +194,12 @@ def validate_child(gateway, node_id, child, value_type=None):
                     exc,
                 )
                 continue
-            dev_id = id(gateway), node_id, child.id, set_req[v_name].value
+            dev_id: DevId = (
+                gateway_id,
+                node_id,
+                child.id,
+                set_req[v_name].value,
+            )
             validated[platform].append(dev_id)
 
     return validated
diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py
index ffbcba6f0329717dd9952fdb762871684e207996..f90f9c5c81c77b178898cad0192431feb92bf4f6 100644
--- a/homeassistant/components/mysensors/light.py
+++ b/homeassistant/components/mysensors/light.py
@@ -1,4 +1,6 @@
 """Support for MySensors lights."""
+from typing import Callable
+
 from homeassistant.components import mysensors
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS,
@@ -10,27 +12,47 @@ from homeassistant.components.light import (
     SUPPORT_WHITE_VALUE,
     LightEntity,
 )
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import STATE_OFF, STATE_ON
 from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
 import homeassistant.util.color as color_util
 from homeassistant.util.color import rgb_hex_to_rgb_list
 
 SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the mysensors platform for lights."""
+async def async_setup_entry(
+    hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+    """Set up this platform for a specific ConfigEntry(==Gateway)."""
     device_class_map = {
         "S_DIMMER": MySensorsLightDimmer,
         "S_RGB_LIGHT": MySensorsLightRGB,
         "S_RGBW_LIGHT": MySensorsLightRGBW,
     }
-    mysensors.setup_mysensors_platform(
+
+    async def async_discover(discovery_info):
+        """Discover and add a MySensors light."""
+        mysensors.setup_mysensors_platform(
+            hass,
+            DOMAIN,
+            discovery_info,
+            device_class_map,
+            async_add_entities=async_add_entities,
+        )
+
+    await on_unload(
         hass,
-        DOMAIN,
-        discovery_info,
-        device_class_map,
-        async_add_entities=async_add_entities,
+        config_entry,
+        async_dispatcher_connect(
+            hass,
+            MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+            async_discover,
+        ),
     )
 
 
@@ -60,11 +82,6 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
         """Return the white value of this light between 0..255."""
         return self._white
 
-    @property
-    def assumed_state(self):
-        """Return true if unable to access real state of entity."""
-        return self.gateway.optimistic
-
     @property
     def is_on(self):
         """Return true if device is on."""
@@ -80,7 +97,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
             self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
         )
 
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # optimistically assume that light has changed state
             self._state = True
             self._values[set_req.V_LIGHT] = STATE_ON
@@ -102,7 +119,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
             self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1
         )
 
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # optimistically assume that light has changed state
             self._brightness = brightness
             self._values[set_req.V_DIMMER] = percent
@@ -135,7 +152,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
             self.node_id, self.child_id, self.value_type, hex_color, ack=1
         )
 
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # optimistically assume that light has changed state
             self._hs = color_util.color_RGB_to_hs(*rgb)
             self._white = white
@@ -145,7 +162,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
         """Turn the device off."""
         value_type = self.gateway.const.SetReq.V_LIGHT
         self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1)
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # optimistically assume that light has changed state
             self._state = False
             self._values[value_type] = STATE_OFF
@@ -188,7 +205,7 @@ class MySensorsLightDimmer(MySensorsLight):
         """Turn the device on."""
         self._turn_on_light()
         self._turn_on_dimmer(**kwargs)
-        if self.gateway.optimistic:
+        if self.assumed_state:
             self.async_write_ha_state()
 
     async def async_update(self):
@@ -214,7 +231,7 @@ class MySensorsLightRGB(MySensorsLight):
         self._turn_on_light()
         self._turn_on_dimmer(**kwargs)
         self._turn_on_rgb_and_w("%02x%02x%02x", **kwargs)
-        if self.gateway.optimistic:
+        if self.assumed_state:
             self.async_write_ha_state()
 
     async def async_update(self):
@@ -241,5 +258,5 @@ class MySensorsLightRGBW(MySensorsLightRGB):
         self._turn_on_light()
         self._turn_on_dimmer(**kwargs)
         self._turn_on_rgb_and_w("%02x%02x%02x%02x", **kwargs)
-        if self.gateway.optimistic:
+        if self.assumed_state:
             self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json
index afeeb5d57cc99bc504398482b4d96ed1ccebeab8..8371f2930c2fb169abe8ccdf9d5437fa7e76f93f 100644
--- a/homeassistant/components/mysensors/manifest.json
+++ b/homeassistant/components/mysensors/manifest.json
@@ -2,7 +2,15 @@
   "domain": "mysensors",
   "name": "MySensors",
   "documentation": "https://www.home-assistant.io/integrations/mysensors",
-  "requirements": ["pymysensors==0.18.0"],
-  "after_dependencies": ["mqtt"],
-  "codeowners": ["@MartinHjelmare"]
+  "requirements": [
+    "pymysensors==0.20.1"
+  ],
+  "after_dependencies": [
+    "mqtt"
+  ],
+  "codeowners": [
+    "@MartinHjelmare",
+    "@functionpointer"
+  ],
+  "config_flow": true
 }
diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py
index bab6bf3fc402e8184921bb5bd34cb08b0157ef53..a09f8af139459ece72575ee7fb1007c0e980e10a 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -1,6 +1,11 @@
 """Support for MySensors sensors."""
+from typing import Callable
+
 from homeassistant.components import mysensors
+from homeassistant.components.mysensors import on_unload
+from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY
 from homeassistant.components.sensor import DOMAIN
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
     CONDUCTIVITY,
     DEGREE,
@@ -18,6 +23,8 @@ from homeassistant.const import (
     VOLT,
     VOLUME_CUBIC_METERS,
 )
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
 
 SENSORS = {
     "V_TEMP": [None, "mdi:thermometer"],
@@ -54,14 +61,29 @@ SENSORS = {
 }
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the MySensors platform for sensors."""
-    mysensors.setup_mysensors_platform(
+async def async_setup_entry(
+    hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+    """Set up this platform for a specific ConfigEntry(==Gateway)."""
+
+    async def async_discover(discovery_info):
+        """Discover and add a MySensors sensor."""
+        mysensors.setup_mysensors_platform(
+            hass,
+            DOMAIN,
+            discovery_info,
+            MySensorsSensor,
+            async_add_entities=async_add_entities,
+        )
+
+    await on_unload(
         hass,
-        DOMAIN,
-        discovery_info,
-        MySensorsSensor,
-        async_add_entities=async_add_entities,
+        config_entry,
+        async_dispatcher_connect(
+            hass,
+            MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+            async_discover,
+        ),
     )
 
 
@@ -105,7 +127,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity):
         pres = self.gateway.const.Presentation
         set_req = self.gateway.const.SetReq
         SENSORS[set_req.V_TEMP.name][0] = (
-            TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT
+            TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
         )
         sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None])
         if isinstance(sensor_type, dict):
diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json
new file mode 100644
index 0000000000000000000000000000000000000000..43a68f61e247a06b85b1ecfe6810c3488c59cfc2
--- /dev/null
+++ b/homeassistant/components/mysensors/strings.json
@@ -0,0 +1,79 @@
+{
+  "title": "MySensors",
+  "config": {
+    "step": {
+      "user": {
+        "data": {
+          "gateway_type": "Gateway type"
+        },
+        "description": "Choose connection method to the gateway"
+      },
+      "gw_tcp": {
+        "description": "Ethernet gateway setup",
+        "data": {
+          "device": "IP address of the gateway",
+          "tcp_port": "port",
+          "version": "MySensors version",
+          "persistence_file": "persistence file (leave empty to auto-generate)"
+        }
+      },
+      "gw_serial": {
+        "description": "Serial gateway setup",
+        "data": {
+          "device": "Serial port",
+          "baud_rate": "baud rate",
+          "version": "MySensors version",
+          "persistence_file": "persistence file (leave empty to auto-generate)"
+        }
+      },
+      "gw_mqtt": {
+        "description": "MQTT gateway setup",
+        "data": {
+          "retain": "mqtt retain",
+          "topic_in_prefix": "prefix for input topics (topic_in_prefix)",
+          "topic_out_prefix": "prefix for output topics (topic_out_prefix)",
+          "version": "MySensors version",
+          "persistence_file": "persistence file (leave empty to auto-generate)"
+        }
+      }
+    },
+    "error": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+      "invalid_subscribe_topic":  "Invalid subscribe topic",
+      "invalid_publish_topic": "Invalid publish topic",
+      "duplicate_topic": "Topic already in use",
+      "same_topic": "Subscribe and publish topics are the same",
+      "invalid_port": "Invalid port number",
+      "invalid_persistence_file": "Invalid persistence file",
+      "duplicate_persistence_file": "Persistence file already in use",
+      "invalid_ip": "Invalid IP address",
+      "invalid_serial": "Invalid serial port",
+      "invalid_device": "Invalid device",
+      "invalid_version": "Invalid MySensors version",
+      "not_a_number": "Please enter a number",
+      "port_out_of_range": "Port number must be at least 1 and at most 65535",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+      "invalid_subscribe_topic":  "Invalid subscribe topic",
+      "invalid_publish_topic": "Invalid publish topic",
+      "duplicate_topic": "Topic already in use",
+      "same_topic": "Subscribe and publish topics are the same",
+      "invalid_port": "Invalid port number",
+      "invalid_persistence_file": "Invalid persistence file",
+      "duplicate_persistence_file": "Persistence file already in use",
+      "invalid_ip": "Invalid IP address",
+      "invalid_serial": "Invalid serial port",
+      "invalid_device": "Invalid device",
+      "invalid_version": "Invalid MySensors version",
+      "not_a_number": "Please enter a number",
+      "port_out_of_range": "Port number must be at least 1 and at most 65535",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    }
+  }
+}
diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py
index 0da8bfe7030f57557347ab6027cdb0d169176d80..14911e11090fb48dcba91aaebf00662f42f25c1e 100644
--- a/homeassistant/components/mysensors/switch.py
+++ b/homeassistant/components/mysensors/switch.py
@@ -1,4 +1,6 @@
 """Support for MySensors switches."""
+from typing import Callable
+
 import voluptuous as vol
 
 from homeassistant.components import mysensors
@@ -6,7 +8,11 @@ from homeassistant.components.switch import DOMAIN, SwitchEntity
 from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
 import homeassistant.helpers.config_validation as cv
 
-from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE
+from . import on_unload
+from ...config_entries import ConfigEntry
+from ...helpers.dispatcher import async_dispatcher_connect
+from ...helpers.typing import HomeAssistantType
+from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE
 
 ATTR_IR_CODE = "V_IR_SEND"
 
@@ -15,8 +21,10 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema(
 )
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the mysensors platform for switches."""
+async def async_setup_entry(
+    hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+):
+    """Set up this platform for a specific ConfigEntry(==Gateway)."""
     device_class_map = {
         "S_DOOR": MySensorsSwitch,
         "S_MOTION": MySensorsSwitch,
@@ -32,13 +40,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         "S_MOISTURE": MySensorsSwitch,
         "S_WATER_QUALITY": MySensorsSwitch,
     }
-    mysensors.setup_mysensors_platform(
-        hass,
-        DOMAIN,
-        discovery_info,
-        device_class_map,
-        async_add_entities=async_add_entities,
-    )
+
+    async def async_discover(discovery_info):
+        """Discover and add a MySensors switch."""
+        mysensors.setup_mysensors_platform(
+            hass,
+            DOMAIN,
+            discovery_info,
+            device_class_map,
+            async_add_entities=async_add_entities,
+        )
 
     async def async_send_ir_code_service(service):
         """Set IR code as device state attribute."""
@@ -71,15 +82,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         schema=SEND_IR_CODE_SERVICE_SCHEMA,
     )
 
+    await on_unload(
+        hass,
+        config_entry,
+        async_dispatcher_connect(
+            hass,
+            MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
+            async_discover,
+        ),
+    )
+
 
 class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
     """Representation of the value of a MySensors Switch child node."""
 
-    @property
-    def assumed_state(self):
-        """Return True if unable to access real state of entity."""
-        return self.gateway.optimistic
-
     @property
     def current_power_w(self):
         """Return the current power usage in W."""
@@ -96,7 +112,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
         self.gateway.set_child_value(
             self.node_id, self.child_id, self.value_type, 1, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that switch has changed state
             self._values[self.value_type] = STATE_ON
             self.async_write_ha_state()
@@ -106,7 +122,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
         self.gateway.set_child_value(
             self.node_id, self.child_id, self.value_type, 0, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that switch has changed state
             self._values[self.value_type] = STATE_OFF
             self.async_write_ha_state()
@@ -137,7 +153,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
         self.gateway.set_child_value(
             self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that switch has changed state
             self._values[self.value_type] = self._ir_code
             self._values[set_req.V_LIGHT] = STATE_ON
@@ -151,7 +167,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
         self.gateway.set_child_value(
             self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1
         )
-        if self.gateway.optimistic:
+        if self.assumed_state:
             # Optimistically assume that switch has changed state
             self._values[set_req.V_LIGHT] = STATE_OFF
             self.async_write_ha_state()
diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..d7730ba09b6e7456d69a9d478ada96b5a16e25b4
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/en.json
@@ -0,0 +1,79 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Device is already configured",
+            "cannot_connect": "Failed to connect",
+            "invalid_subscribe_topic":  "Invalid subscribe topic",
+            "invalid_publish_topic": "Invalid publish topic",
+            "duplicate_topic": "Topic already in use",
+            "same_topic": "Subscribe and publish topics are the same",
+            "invalid_port": "Invalid port number",
+            "invalid_persistence_file": "Invalid persistence file",
+            "duplicate_persistence_file": "Persistence file already in use",
+            "invalid_ip": "Invalid IP address",
+            "invalid_serial": "Invalid serial port",
+            "invalid_device": "Invalid device",
+            "invalid_version": "Invalid MySensors version",
+            "not_a_number": "Please enter a number",
+            "port_out_of_range": "Port number must be at least 1 and at most 65535",
+            "unknown": "Unexpected error"
+        },
+        "error": {
+            "already_configured": "Device is already configured",
+            "cannot_connect": "Failed to connect",
+            "invalid_subscribe_topic":  "Invalid subscribe topic",
+            "invalid_publish_topic": "Invalid publish topic",
+            "duplicate_topic": "Topic already in use",
+            "same_topic": "Subscribe and publish topics are the same",
+            "invalid_port": "Invalid port number",
+            "invalid_persistence_file": "Invalid persistence file",
+            "duplicate_persistence_file": "Persistence file already in use",
+            "invalid_ip": "Invalid IP address",
+            "invalid_serial": "Invalid serial port",
+            "invalid_device": "Invalid device",
+            "invalid_version": "Invalid MySensors version",
+            "not_a_number": "Please enter a number",
+            "port_out_of_range": "Port number must be at least 1 and at most 65535",
+            "unknown": "Unexpected error"
+        },
+        "step": {
+            "user": {
+                "data": {
+                "optimistic": "optimistic",
+                "persistence": "persistence",
+                "gateway_type": "Gateway type"
+                },
+                "description": "Choose connection method to the gateway"
+            },
+            "gw_tcp": {
+                "description": "Ethernet gateway setup",
+                "data": {
+                    "device": "IP address of the gateway",
+                    "tcp_port": "port",
+                    "version": "MySensors version",
+                    "persistence_file": "persistence file (leave empty to auto-generate)"
+                }
+            },
+            "gw_serial": {
+                "description": "Serial gateway setup",
+                "data": {
+                    "device": "Serial port",
+                    "baud_rate": "baud rate",
+                    "version": "MySensors version",
+                    "persistence_file": "persistence file (leave empty to auto-generate)"
+                }
+            },
+            "gw_mqtt": {
+                "description": "MQTT gateway setup",
+                "data": {
+                    "retain": "mqtt retain",
+                    "topic_in_prefix": "prefix for input topics (topic_in_prefix)",
+                    "topic_out_prefix": "prefix for output topics (topic_out_prefix)",
+                    "version": "MySensors version",
+                    "persistence_file": "persistence file (leave empty to auto-generate)"
+                }
+            }
+        }
+    },
+    "title": "MySensors"
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index c2e36d9f846872993eb69325c06ca4d01c87af2b..6366a3eb8879c6d11972bf3355b8cef9dff171f3 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -136,6 +136,7 @@ FLOWS = [
     "motion_blinds",
     "mqtt",
     "myq",
+    "mysensors",
     "neato",
     "nest",
     "netatmo",
diff --git a/requirements_all.txt b/requirements_all.txt
index 2a78b25a162418763ed1cc20dde961f3b4216a90..2cc29c3be4b5e6c404e7509662de14d7069dca95 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1551,7 +1551,7 @@ pymusiccast==0.1.6
 pymyq==2.0.14
 
 # homeassistant.components.mysensors
-pymysensors==0.18.0
+pymysensors==0.20.1
 
 # homeassistant.components.nanoleaf
 pynanoleaf==0.0.5
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 024de6596b654be2c221d6e6566c602b5e54f818..2a447b539ef58b919bac5d17e6e3c3c03086f554 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -810,6 +810,9 @@ pymonoprice==0.3
 # homeassistant.components.myq
 pymyq==2.0.14
 
+# homeassistant.components.mysensors
+pymysensors==0.20.1
+
 # homeassistant.components.nuki
 pynuki==1.3.8
 
diff --git a/tests/components/mysensors/__init__.py b/tests/components/mysensors/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..68fc6d7b4d705324a95e38522d9695335aa87701
--- /dev/null
+++ b/tests/components/mysensors/__init__.py
@@ -0,0 +1 @@
+"""Tests for the MySensors integration."""
diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..6bfec3b102e585598e2752801423749279964ed6
--- /dev/null
+++ b/tests/components/mysensors/test_config_flow.py
@@ -0,0 +1,735 @@
+"""Test the MySensors config flow."""
+from typing import Dict, Optional, Tuple
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, setup
+from homeassistant.components.mysensors.const import (
+    CONF_BAUD_RATE,
+    CONF_DEVICE,
+    CONF_GATEWAY_TYPE,
+    CONF_GATEWAY_TYPE_MQTT,
+    CONF_GATEWAY_TYPE_SERIAL,
+    CONF_GATEWAY_TYPE_TCP,
+    CONF_PERSISTENCE,
+    CONF_PERSISTENCE_FILE,
+    CONF_RETAIN,
+    CONF_TCP_PORT,
+    CONF_TOPIC_IN_PREFIX,
+    CONF_TOPIC_OUT_PREFIX,
+    CONF_VERSION,
+    DOMAIN,
+    ConfGatewayType,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.common import MockConfigEntry
+
+
+async def get_form(
+    hass: HomeAssistantType, gatway_type: ConfGatewayType, expected_step_id: str
+):
+    """Get a form for the given gateway type."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+    stepuser = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+    assert stepuser["type"] == "form"
+    assert not stepuser["errors"]
+
+    result = await hass.config_entries.flow.async_configure(
+        stepuser["flow_id"],
+        {CONF_GATEWAY_TYPE: gatway_type},
+    )
+    await hass.async_block_till_done()
+    assert result["type"] == "form"
+    assert result["step_id"] == expected_step_id
+
+    return result
+
+
+async def test_config_mqtt(hass: HomeAssistantType):
+    """Test configuring a mqtt gateway."""
+    step = await get_form(hass, CONF_GATEWAY_TYPE_MQTT, "gw_mqtt")
+    flow_id = step["flow_id"]
+
+    with patch(
+        "homeassistant.components.mysensors.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            flow_id,
+            {
+                CONF_RETAIN: True,
+                CONF_TOPIC_IN_PREFIX: "bla",
+                CONF_TOPIC_OUT_PREFIX: "blub",
+                CONF_VERSION: "2.4",
+            },
+        )
+        await hass.async_block_till_done()
+
+    if "errors" in result2:
+        assert not result2["errors"]
+    assert result2["type"] == "create_entry"
+    assert result2["title"] == "mqtt"
+    assert result2["data"] == {
+        CONF_DEVICE: "mqtt",
+        CONF_RETAIN: True,
+        CONF_TOPIC_IN_PREFIX: "bla",
+        CONF_TOPIC_OUT_PREFIX: "blub",
+        CONF_VERSION: "2.4",
+    }
+    assert len(mock_setup.mock_calls) == 1
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_config_serial(hass: HomeAssistantType):
+    """Test configuring a gateway via serial."""
+    step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial")
+    flow_id = step["flow_id"]
+
+    with patch(  # mock is_serial_port because otherwise the test will be platform dependent (/dev/ttyACMx vs COMx)
+        "homeassistant.components.mysensors.config_flow.is_serial_port",
+        return_value=True,
+    ), patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+    ), patch(
+        "homeassistant.components.mysensors.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            flow_id,
+            {
+                CONF_BAUD_RATE: 115200,
+                CONF_DEVICE: "/dev/ttyACM0",
+                CONF_VERSION: "2.4",
+            },
+        )
+        await hass.async_block_till_done()
+
+    if "errors" in result2:
+        assert not result2["errors"]
+    assert result2["type"] == "create_entry"
+    assert result2["title"] == "/dev/ttyACM0"
+    assert result2["data"] == {
+        CONF_DEVICE: "/dev/ttyACM0",
+        CONF_BAUD_RATE: 115200,
+        CONF_VERSION: "2.4",
+    }
+    assert len(mock_setup.mock_calls) == 1
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_config_tcp(hass: HomeAssistantType):
+    """Test configuring a gateway via tcp."""
+    step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
+    flow_id = step["flow_id"]
+
+    with patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+    ), patch(
+        "homeassistant.components.mysensors.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            flow_id,
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "2.4",
+            },
+        )
+        await hass.async_block_till_done()
+
+    if "errors" in result2:
+        assert not result2["errors"]
+    assert result2["type"] == "create_entry"
+    assert result2["title"] == "127.0.0.1"
+    assert result2["data"] == {
+        CONF_DEVICE: "127.0.0.1",
+        CONF_TCP_PORT: 5003,
+        CONF_VERSION: "2.4",
+    }
+    assert len(mock_setup.mock_calls) == 1
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_fail_to_connect(hass: HomeAssistantType):
+    """Test configuring a gateway via tcp."""
+    step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp")
+    flow_id = step["flow_id"]
+
+    with patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=False
+    ), patch(
+        "homeassistant.components.mysensors.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            flow_id,
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "2.4",
+            },
+        )
+        await hass.async_block_till_done()
+
+    assert result2["type"] == "form"
+    assert "errors" in result2
+    assert "base" in result2["errors"]
+    assert result2["errors"]["base"] == "cannot_connect"
+    assert len(mock_setup.mock_calls) == 0
+    assert len(mock_setup_entry.mock_calls) == 0
+
+
+@pytest.mark.parametrize(
+    "gateway_type, expected_step_id, user_input, err_field, err_string",
+    [
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 600_000,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "2.4",
+            },
+            CONF_TCP_PORT,
+            "port_out_of_range",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 0,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "2.4",
+            },
+            CONF_TCP_PORT,
+            "port_out_of_range",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "a",
+            },
+            CONF_VERSION,
+            "invalid_version",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "a.b",
+            },
+            CONF_VERSION,
+            "invalid_version",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+            },
+            CONF_VERSION,
+            "invalid_version",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "4",
+            },
+            CONF_VERSION,
+            "invalid_version",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_VERSION: "v3",
+            },
+            CONF_VERSION,
+            "invalid_version",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "127.0.0.",
+            },
+            CONF_DEVICE,
+            "invalid_ip",
+        ),
+        (
+            CONF_GATEWAY_TYPE_TCP,
+            "gw_tcp",
+            {
+                CONF_TCP_PORT: 5003,
+                CONF_DEVICE: "abcd",
+            },
+            CONF_DEVICE,
+            "invalid_ip",
+        ),
+        (
+            CONF_GATEWAY_TYPE_MQTT,
+            "gw_mqtt",
+            {
+                CONF_RETAIN: True,
+                CONF_TOPIC_IN_PREFIX: "bla",
+                CONF_TOPIC_OUT_PREFIX: "blub",
+                CONF_PERSISTENCE_FILE: "asdf.zip",
+                CONF_VERSION: "2.4",
+            },
+            CONF_PERSISTENCE_FILE,
+            "invalid_persistence_file",
+        ),
+        (
+            CONF_GATEWAY_TYPE_MQTT,
+            "gw_mqtt",
+            {
+                CONF_RETAIN: True,
+                CONF_TOPIC_IN_PREFIX: "/#/#",
+                CONF_TOPIC_OUT_PREFIX: "blub",
+                CONF_VERSION: "2.4",
+            },
+            CONF_TOPIC_IN_PREFIX,
+            "invalid_subscribe_topic",
+        ),
+        (
+            CONF_GATEWAY_TYPE_MQTT,
+            "gw_mqtt",
+            {
+                CONF_RETAIN: True,
+                CONF_TOPIC_IN_PREFIX: "asdf",
+                CONF_TOPIC_OUT_PREFIX: "/#/#",
+                CONF_VERSION: "2.4",
+            },
+            CONF_TOPIC_OUT_PREFIX,
+            "invalid_publish_topic",
+        ),
+        (
+            CONF_GATEWAY_TYPE_MQTT,
+            "gw_mqtt",
+            {
+                CONF_RETAIN: True,
+                CONF_TOPIC_IN_PREFIX: "asdf",
+                CONF_TOPIC_OUT_PREFIX: "asdf",
+                CONF_VERSION: "2.4",
+            },
+            CONF_TOPIC_OUT_PREFIX,
+            "same_topic",
+        ),
+    ],
+)
+async def test_config_invalid(
+    hass: HomeAssistantType,
+    gateway_type: ConfGatewayType,
+    expected_step_id: str,
+    user_input: Dict[str, any],
+    err_field,
+    err_string,
+):
+    """Perform a test that is expected to generate an error."""
+    step = await get_form(hass, gateway_type, expected_step_id)
+    flow_id = step["flow_id"]
+
+    with patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+    ), patch(
+        "homeassistant.components.mysensors.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            flow_id,
+            user_input,
+        )
+        await hass.async_block_till_done()
+
+    assert result2["type"] == "form"
+    assert "errors" in result2
+    assert err_field in result2["errors"]
+    assert result2["errors"][err_field] == err_string
+    assert len(mock_setup.mock_calls) == 0
+    assert len(mock_setup_entry.mock_calls) == 0
+
+
+@pytest.mark.parametrize(
+    "user_input",
+    [
+        {
+            CONF_DEVICE: "COM5",
+            CONF_BAUD_RATE: 57600,
+            CONF_TCP_PORT: 5003,
+            CONF_RETAIN: True,
+            CONF_VERSION: "2.3",
+            CONF_PERSISTENCE_FILE: "bla.json",
+        },
+        {
+            CONF_DEVICE: "COM5",
+            CONF_PERSISTENCE_FILE: "bla.json",
+            CONF_BAUD_RATE: 57600,
+            CONF_TCP_PORT: 5003,
+            CONF_VERSION: "2.3",
+            CONF_PERSISTENCE: False,
+            CONF_RETAIN: True,
+        },
+        {
+            CONF_DEVICE: "mqtt",
+            CONF_BAUD_RATE: 115200,
+            CONF_TCP_PORT: 5003,
+            CONF_TOPIC_IN_PREFIX: "intopic",
+            CONF_TOPIC_OUT_PREFIX: "outtopic",
+            CONF_VERSION: "2.4",
+            CONF_PERSISTENCE: False,
+            CONF_RETAIN: False,
+        },
+        {
+            CONF_DEVICE: "127.0.0.1",
+            CONF_PERSISTENCE_FILE: "blub.pickle",
+            CONF_BAUD_RATE: 115200,
+            CONF_TCP_PORT: 343,
+            CONF_VERSION: "2.4",
+            CONF_PERSISTENCE: False,
+            CONF_RETAIN: False,
+        },
+    ],
+)
+async def test_import(hass: HomeAssistantType, user_input: Dict):
+    """Test importing a gateway."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    with patch("sys.platform", "win32"), patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+    ), patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN, data=user_input, context={"source": config_entries.SOURCE_IMPORT}
+        )
+        await hass.async_block_till_done()
+
+    assert result["type"] == "create_entry"
+
+
+@pytest.mark.parametrize(
+    "first_input, second_input, expected_result",
+    [
+        (
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "same2",
+            },
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "same2",
+            },
+            (CONF_TOPIC_IN_PREFIX, "duplicate_topic"),
+        ),
+        (
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "different1",
+                CONF_TOPIC_OUT_PREFIX: "different2",
+            },
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "different3",
+                CONF_TOPIC_OUT_PREFIX: "different4",
+            },
+            None,
+        ),
+        (
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "different2",
+            },
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "different4",
+            },
+            (CONF_TOPIC_IN_PREFIX, "duplicate_topic"),
+        ),
+        (
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "different2",
+            },
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "different1",
+                CONF_TOPIC_OUT_PREFIX: "same1",
+            },
+            (CONF_TOPIC_OUT_PREFIX, "duplicate_topic"),
+        ),
+        (
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "different2",
+            },
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: "2.3",
+                CONF_TOPIC_IN_PREFIX: "same1",
+                CONF_TOPIC_OUT_PREFIX: "different1",
+            },
+            (CONF_TOPIC_IN_PREFIX, "duplicate_topic"),
+        ),
+        (
+            {
+                CONF_DEVICE: "127.0.0.1",
+                CONF_PERSISTENCE_FILE: "same.json",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_PERSISTENCE_FILE: "same.json",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            ("persistence_file", "duplicate_persistence_file"),
+        ),
+        (
+            {
+                CONF_DEVICE: "127.0.0.1",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_PERSISTENCE_FILE: "same.json",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            None,
+        ),
+        (
+            {
+                CONF_DEVICE: "127.0.0.1",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            None,
+        ),
+        (
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_PERSISTENCE_FILE: "different1.json",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_PERSISTENCE_FILE: "different2.json",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            ("base", "already_configured"),
+        ),
+        (
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_PERSISTENCE_FILE: "different1.json",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_PERSISTENCE_FILE: "different2.json",
+                CONF_TCP_PORT: 5003,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            None,
+        ),
+        (
+            {
+                CONF_DEVICE: "192.168.1.2",
+                CONF_TCP_PORT: 5003,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            {
+                CONF_DEVICE: "192.168.1.3",
+                CONF_TCP_PORT: 5003,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE: False,
+                CONF_RETAIN: False,
+            },
+            None,
+        ),
+        (
+            {
+                CONF_DEVICE: "COM5",
+                CONF_TCP_PORT: 5003,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE_FILE: "different1.json",
+            },
+            {
+                CONF_DEVICE: "COM5",
+                CONF_TCP_PORT: 5003,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE_FILE: "different2.json",
+            },
+            ("base", "already_configured"),
+        ),
+        (
+            {
+                CONF_DEVICE: "COM6",
+                CONF_BAUD_RATE: 57600,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+            },
+            {
+                CONF_DEVICE: "COM5",
+                CONF_TCP_PORT: 5003,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+            },
+            None,
+        ),
+        (
+            {
+                CONF_DEVICE: "COM5",
+                CONF_BAUD_RATE: 115200,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE_FILE: "different1.json",
+            },
+            {
+                CONF_DEVICE: "COM5",
+                CONF_BAUD_RATE: 57600,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE_FILE: "different2.json",
+            },
+            ("base", "already_configured"),
+        ),
+        (
+            {
+                CONF_DEVICE: "COM5",
+                CONF_BAUD_RATE: 115200,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE_FILE: "same.json",
+            },
+            {
+                CONF_DEVICE: "COM6",
+                CONF_BAUD_RATE: 57600,
+                CONF_RETAIN: True,
+                CONF_VERSION: "2.3",
+                CONF_PERSISTENCE_FILE: "same.json",
+            },
+            ("persistence_file", "duplicate_persistence_file"),
+        ),
+        (
+            {
+                CONF_DEVICE: "mqtt",
+                CONF_PERSISTENCE_FILE: "bla.json",
+                CONF_BAUD_RATE: 115200,
+                CONF_TCP_PORT: 5003,
+                CONF_VERSION: "1.4",
+            },
+            {
+                CONF_DEVICE: "COM6",
+                CONF_PERSISTENCE_FILE: "bla2.json",
+                CONF_BAUD_RATE: 115200,
+                CONF_TCP_PORT: 5003,
+                CONF_VERSION: "1.4",
+            },
+            None,
+        ),
+    ],
+)
+async def test_duplicate(
+    hass: HomeAssistantType,
+    first_input: Dict,
+    second_input: Dict,
+    expected_result: Optional[Tuple[str, str]],
+):
+    """Test duplicate detection."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    with patch("sys.platform", "win32"), patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+    ), patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ):
+        MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass)
+
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN, data=second_input, context={"source": config_entries.SOURCE_IMPORT}
+        )
+        await hass.async_block_till_done()
+        if expected_result is None:
+            assert result["type"] == "create_entry"
+        else:
+            assert result["type"] == "abort"
+            assert result["reason"] == expected_result[1]
diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3e360e0b9f8a5a38bd9cbdc046266490ffcc54e
--- /dev/null
+++ b/tests/components/mysensors/test_gateway.py
@@ -0,0 +1,30 @@
+"""Test function in gateway.py."""
+from unittest.mock import patch
+
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.mysensors.gateway import is_serial_port
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+@pytest.mark.parametrize(
+    "port, expect_valid",
+    [
+        ("COM5", True),
+        ("asdf", False),
+        ("COM17", True),
+        ("COM", False),
+        ("/dev/ttyACM0", False),
+    ],
+)
+def test_is_serial_port_windows(hass: HomeAssistantType, port: str, expect_valid: bool):
+    """Test windows serial port."""
+
+    with patch("sys.platform", "win32"):
+        try:
+            is_serial_port(port)
+        except vol.Invalid:
+            assert not expect_valid
+        else:
+            assert expect_valid
diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py
new file mode 100644
index 0000000000000000000000000000000000000000..2775b73efd62991efb3069291a391f57c5d6ceb8
--- /dev/null
+++ b/tests/components/mysensors/test_init.py
@@ -0,0 +1,251 @@
+"""Test function in __init__.py."""
+from typing import Dict
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.mysensors import (
+    CONF_BAUD_RATE,
+    CONF_DEVICE,
+    CONF_GATEWAYS,
+    CONF_PERSISTENCE,
+    CONF_PERSISTENCE_FILE,
+    CONF_RETAIN,
+    CONF_TCP_PORT,
+    CONF_VERSION,
+    DEFAULT_VERSION,
+    DOMAIN,
+)
+from homeassistant.components.mysensors.const import (
+    CONF_GATEWAY_TYPE,
+    CONF_GATEWAY_TYPE_MQTT,
+    CONF_GATEWAY_TYPE_SERIAL,
+    CONF_GATEWAY_TYPE_TCP,
+    CONF_TOPIC_IN_PREFIX,
+    CONF_TOPIC_OUT_PREFIX,
+)
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.setup import async_setup_component
+
+
+@pytest.mark.parametrize(
+    "config, expected_calls, expected_to_succeed, expected_config_flow_user_input",
+    [
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "COM5",
+                            CONF_PERSISTENCE_FILE: "bla.json",
+                            CONF_BAUD_RATE: 57600,
+                            CONF_TCP_PORT: 5003,
+                        }
+                    ],
+                    CONF_VERSION: "2.3",
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: True,
+                }
+            },
+            1,
+            True,
+            {
+                CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL,
+                CONF_DEVICE: "COM5",
+                CONF_PERSISTENCE_FILE: "bla.json",
+                CONF_BAUD_RATE: 57600,
+                CONF_VERSION: "2.3",
+            },
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "127.0.0.1",
+                            CONF_PERSISTENCE_FILE: "blub.pickle",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 343,
+                        }
+                    ],
+                    CONF_VERSION: "2.4",
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            1,
+            True,
+            {
+                CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_PERSISTENCE_FILE: "blub.pickle",
+                CONF_TCP_PORT: 343,
+                CONF_VERSION: "2.4",
+            },
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "127.0.0.1",
+                        }
+                    ],
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            1,
+            True,
+            {
+                CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP,
+                CONF_DEVICE: "127.0.0.1",
+                CONF_TCP_PORT: 5003,
+                CONF_VERSION: DEFAULT_VERSION,
+            },
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "mqtt",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                            CONF_TOPIC_IN_PREFIX: "intopic",
+                            CONF_TOPIC_OUT_PREFIX: "outtopic",
+                        }
+                    ],
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            1,
+            True,
+            {
+                CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT,
+                CONF_DEVICE: "mqtt",
+                CONF_VERSION: DEFAULT_VERSION,
+                CONF_TOPIC_OUT_PREFIX: "outtopic",
+                CONF_TOPIC_IN_PREFIX: "intopic",
+            },
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "mqtt",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                        }
+                    ],
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            0,
+            True,
+            {},
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "mqtt",
+                            CONF_PERSISTENCE_FILE: "bla.json",
+                            CONF_TOPIC_OUT_PREFIX: "out",
+                            CONF_TOPIC_IN_PREFIX: "in",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                        },
+                        {
+                            CONF_DEVICE: "COM6",
+                            CONF_PERSISTENCE_FILE: "bla2.json",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                        },
+                    ],
+                    CONF_VERSION: "2.4",
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            2,
+            True,
+            {},
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "mqtt",
+                            CONF_PERSISTENCE_FILE: "bla.json",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                        },
+                        {
+                            CONF_DEVICE: "COM6",
+                            CONF_PERSISTENCE_FILE: "bla.json",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                        },
+                    ],
+                    CONF_VERSION: "2.4",
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            0,
+            False,
+            {},
+        ),
+        (
+            {
+                DOMAIN: {
+                    CONF_GATEWAYS: [
+                        {
+                            CONF_DEVICE: "COMx",
+                            CONF_PERSISTENCE_FILE: "bla.json",
+                            CONF_BAUD_RATE: 115200,
+                            CONF_TCP_PORT: 5003,
+                        },
+                    ],
+                    CONF_VERSION: "2.4",
+                    CONF_PERSISTENCE: False,
+                    CONF_RETAIN: False,
+                }
+            },
+            0,
+            True,
+            {},
+        ),
+    ],
+)
+async def test_import(
+    hass: HomeAssistantType,
+    config: ConfigType,
+    expected_calls: int,
+    expected_to_succeed: bool,
+    expected_config_flow_user_input: Dict[str, any],
+):
+    """Test importing a gateway."""
+    with patch("sys.platform", "win32"), patch(
+        "homeassistant.components.mysensors.config_flow.try_connect", return_value=True
+    ), patch(
+        "homeassistant.components.mysensors.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result = await async_setup_component(hass, DOMAIN, config)
+        assert result == expected_to_succeed
+        await hass.async_block_till_done()
+
+    assert len(mock_setup_entry.mock_calls) == expected_calls
+
+    if expected_calls > 0:
+        config_flow_user_input = mock_setup_entry.mock_calls[0][1][1].data
+        for key, value in expected_config_flow_user_input.items():
+            assert key in config_flow_user_input
+            assert config_flow_user_input[key] == value