From c5041b41c836f20f5fccddb64a235de50a39055d Mon Sep 17 00:00:00 2001
From: Rob Bierbooms <mail@robbierbooms.nl>
Date: Thu, 1 Oct 2020 08:55:57 +0200
Subject: [PATCH] Implement config and option flow for rfxtrx integration
 (#39117)

* Create option flow for Rfxtrx integration (#37982)

* Implement config flow for rfxtrx integration (#39299)

* Add config flow

* Add strings

* Add first series of tests

* Add tests

* Adjust tests according review comments

* Adjust strings

* Add executor for testing connection

* Change ports to dict

* Fix pylint issue

* Adjust tests

* Migrate config entry for rfxtrx integration (#39528)

* Add rfxtrx device connection validation when importing (#39582)

* Implement import connection validation

* Fix binary sensor tests

* Move rfxtrx data

* Fix cover tests

* Fix test init

* Fix light tests

* Fix sensor tests

* Fix switch tests

* Refactor rfxtrx test data

* Fix strings

* Fix check

* Rework device string in test code

* Add option to delete multiple rfxtrx devices (#39625)

* Opt to remove multiple devices

* Fix devices key

* Add tests (phase 1)

* Add tests (phase 2)

* Tweak remove devices test

* Implement device migration function in rfxtrx option flow (#39694)

* Prompt option to replace device

* Revert unwanted changes

* Add replace device function

* WIP replace entities

* Remove device/entities and update config entry

* Fix styling

* Add info

* Add test

* Fix strings

* Refactor building migration map

* Allow migration for all device types

* Add test to migrate control device

* Fixup some names

* Fixup entry names in test code

* Bump pyRFXtrx to 0.26 and deprecate debug config key (#40679)

* Create option flow for Rfxtrx integration (#37982)

* Implement config flow for rfxtrx integration (#39299)

* Add config flow

* Add strings

* Add first series of tests

* Add tests

* Adjust tests according review comments

* Adjust strings

* Add executor for testing connection

* Change ports to dict

* Fix pylint issue

* Adjust tests

* Migrate config entry for rfxtrx integration (#39528)

* Add rfxtrx device connection validation when importing (#39582)

* Implement import connection validation

* Fix binary sensor tests

* Move rfxtrx data

* Fix cover tests

* Fix test init

* Fix light tests

* Fix sensor tests

* Fix switch tests

* Refactor rfxtrx test data

* Fix strings

* Fix check

* Rework device string in test code

* Add option to delete multiple rfxtrx devices (#39625)

* Opt to remove multiple devices

* Fix devices key

* Add tests (phase 1)

* Add tests (phase 2)

* Tweak remove devices test

* Implement device migration function in rfxtrx option flow (#39694)

* Prompt option to replace device

* Revert unwanted changes

* Add replace device function

* WIP replace entities

* Remove device/entities and update config entry

* Fix styling

* Add info

* Add test

* Fix strings

* Refactor building migration map

* Allow migration for all device types

* Add test to migrate control device

* Fixup some names

* Fixup entry names in test code

* Bump version number

* Remove debug key from connect

* Remove debug option from config flow

* Remove debug from tests

* Fix event test

* Add cv.deprecated

* Fix test

* Fix config schema

* Add timeout on connection

* Rework config schema

* Fix schema...again

* Prevent creation of duplicate device in rfxtrx option flow (#40656)
---
 CODEOWNERS                                    |    2 +-
 homeassistant/components/rfxtrx/__init__.py   |   38 +-
 .../components/rfxtrx/binary_sensor.py        |   22 +-
 .../components/rfxtrx/config_flow.py          |  583 ++++++++-
 homeassistant/components/rfxtrx/const.py      |    9 +
 homeassistant/components/rfxtrx/cover.py      |    8 +-
 homeassistant/components/rfxtrx/light.py      |   14 +-
 homeassistant/components/rfxtrx/manifest.json |    8 +-
 homeassistant/components/rfxtrx/strings.json  |   77 +-
 homeassistant/components/rfxtrx/switch.py     |   18 +-
 .../components/rfxtrx/translations/en.json    |   73 +-
 homeassistant/generated/config_flows.py       |    1 +
 requirements_all.txt                          |    2 +-
 requirements_test_all.txt                     |    2 +-
 tests/components/rfxtrx/conftest.py           |   26 +-
 tests/components/rfxtrx/test_binary_sensor.py |  166 ++-
 tests/components/rfxtrx/test_config_flow.py   | 1125 ++++++++++++++++-
 tests/components/rfxtrx/test_cover.py         |   73 +-
 tests/components/rfxtrx/test_init.py          |   38 +-
 tests/components/rfxtrx/test_light.py         |   69 +-
 tests/components/rfxtrx/test_sensor.py        |  121 +-
 tests/components/rfxtrx/test_switch.py        |   84 +-
 22 files changed, 2199 insertions(+), 360 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index ede90722253..549f4193c09 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -355,7 +355,7 @@ homeassistant/components/rainmachine/* @bachya
 homeassistant/components/random/* @fabaff
 homeassistant/components/rejseplanen/* @DarkFox
 homeassistant/components/repetier/* @MTrab
-homeassistant/components/rfxtrx/* @danielhiversen @elupus
+homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
 homeassistant/components/ring/* @balloob
 homeassistant/components/risco/* @OnFreund
 homeassistant/components/rmvtransport/* @cgtobi
diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py
index eb13800f748..c12a6380f20 100644
--- a/homeassistant/components/rfxtrx/__init__.py
+++ b/homeassistant/components/rfxtrx/__init__.py
@@ -5,6 +5,7 @@ from collections import OrderedDict
 import logging
 
 import RFXtrx as rfxtrxmod
+import async_timeout
 import voluptuous as vol
 
 from homeassistant import config_entries
@@ -33,11 +34,19 @@ from homeassistant.const import (
     VOLT,
 )
 from homeassistant.core import callback
+from homeassistant.exceptions import ConfigEntryNotReady
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.restore_state import RestoreEntity
 
 from .const import (
     ATTR_EVENT,
+    CONF_AUTOMATIC_ADD,
+    CONF_DATA_BITS,
+    CONF_DEBUG,
+    CONF_FIRE_EVENT,
+    CONF_OFF_DELAY,
+    CONF_REMOVE_DEVICE,
+    CONF_SIGNAL_REPETITIONS,
     DEVICE_PACKET_TYPE_LIGHTING4,
     EVENT_RFXTRX_EVENT,
     SERVICE_SEND,
@@ -47,12 +56,6 @@ DOMAIN = "rfxtrx"
 
 DEFAULT_SIGNAL_REPETITIONS = 1
 
-CONF_FIRE_EVENT = "fire_event"
-CONF_DATA_BITS = "data_bits"
-CONF_AUTOMATIC_ADD = "automatic_add"
-CONF_SIGNAL_REPETITIONS = "signal_repetitions"
-CONF_DEBUG = "debug"
-CONF_OFF_DELAY = "off_delay"
 SIGNAL_EVENT = f"{DOMAIN}_event"
 
 DATA_TYPES = OrderedDict(
@@ -126,10 +129,10 @@ DEVICE_DATA_SCHEMA = vol.Schema(
 
 BASE_SCHEMA = vol.Schema(
     {
-        vol.Optional(CONF_DEBUG, default=False): cv.boolean,
+        vol.Optional(CONF_DEBUG): cv.boolean,
         vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
         vol.Optional(CONF_DEVICES, default={}): {cv.string: _ensure_device},
-    }
+    },
 )
 
 DEVICE_SCHEMA = BASE_SCHEMA.extend({vol.Required(CONF_DEVICE): cv.string})
@@ -139,7 +142,8 @@ PORT_SCHEMA = BASE_SCHEMA.extend(
 )
 
 CONFIG_SCHEMA = vol.Schema(
-    {DOMAIN: vol.Any(DEVICE_SCHEMA, PORT_SCHEMA)}, extra=vol.ALLOW_EXTRA
+    {DOMAIN: vol.All(cv.deprecated(CONF_DEBUG), vol.Any(DEVICE_SCHEMA, PORT_SCHEMA))},
+    extra=vol.ALLOW_EXTRA,
 )
 
 DOMAINS = ["switch", "sensor", "light", "binary_sensor", "cover"]
@@ -154,7 +158,6 @@ async def async_setup(hass, config):
         CONF_HOST: config[DOMAIN].get(CONF_HOST),
         CONF_PORT: config[DOMAIN].get(CONF_PORT),
         CONF_DEVICE: config[DOMAIN].get(CONF_DEVICE),
-        CONF_DEBUG: config[DOMAIN].get(CONF_DEBUG),
         CONF_AUTOMATIC_ADD: config[DOMAIN].get(CONF_AUTOMATIC_ADD),
         CONF_DEVICES: config[DOMAIN][CONF_DEVICES],
     }
@@ -223,11 +226,10 @@ def _create_rfx(config):
         rfx = rfxtrxmod.Connect(
             (config[CONF_HOST], config[CONF_PORT]),
             None,
-            debug=config[CONF_DEBUG],
             transport_protocol=rfxtrxmod.PyNetworkTransport,
         )
     else:
-        rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None, debug=config[CONF_DEBUG])
+        rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None)
 
     return rfx
 
@@ -251,7 +253,11 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry):
     config = entry.data
 
     # Initialize library
-    rfx_object = await hass.async_add_executor_job(_create_rfx, config)
+    try:
+        async with async_timeout.timeout(5):
+            rfx_object = await hass.async_add_executor_job(_create_rfx, config)
+    except asyncio.TimeoutError as err:
+        raise ConfigEntryNotReady from err
 
     # Setup some per device config
     devices = _get_device_lookup(config[CONF_DEVICES])
@@ -444,6 +450,12 @@ class RfxtrxEntity(RestoreEntity):
             )
         )
 
+        self.async_on_remove(
+            self.hass.helpers.dispatcher.async_dispatcher_connect(
+                f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", self.async_remove
+            )
+        )
+
     @property
     def should_poll(self):
         """No polling needed for a RFXtrx switch."""
diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py
index 21f3e0b74b3..7fe89e747bc 100644
--- a/homeassistant/components/rfxtrx/binary_sensor.py
+++ b/homeassistant/components/rfxtrx/binary_sensor.py
@@ -61,6 +61,18 @@ DEVICE_TYPE_DEVICE_CLASS = {
 }
 
 
+def supported(event):
+    """Return whether an event supports binary_sensor."""
+    if isinstance(event, rfxtrxmod.ControlEvent):
+        return True
+    if isinstance(event, rfxtrxmod.SensorEvent):
+        return event.values.get("Sensor Status") in [
+            *SENSOR_STATUS_ON,
+            *SENSOR_STATUS_OFF,
+        ]
+    return False
+
+
 async def async_setup_entry(
     hass,
     config_entry,
@@ -74,16 +86,6 @@ async def async_setup_entry(
 
     discovery_info = config_entry.data
 
-    def supported(event):
-        if isinstance(event, rfxtrxmod.ControlEvent):
-            return True
-        if isinstance(event, rfxtrxmod.SensorEvent):
-            return event.values.get("Sensor Status") in [
-                *SENSOR_STATUS_ON,
-                *SENSOR_STATUS_OFF,
-            ]
-        return False
-
     for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
         event = get_rfx_object(packet_id)
         if event is None:
diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py
index 596f1d0b5e9..db7ca49691a 100644
--- a/homeassistant/components/rfxtrx/config_flow.py
+++ b/homeassistant/components/rfxtrx/config_flow.py
@@ -1,12 +1,404 @@
 """Config flow for RFXCOM RFXtrx integration."""
+import copy
 import logging
+import os
 
-from homeassistant import config_entries
+import RFXtrx as rfxtrxmod
+import serial
+import serial.tools.list_ports
+import voluptuous as vol
 
-from . import DOMAIN
+from homeassistant import config_entries, exceptions
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    CONF_COMMAND_OFF,
+    CONF_COMMAND_ON,
+    CONF_DEVICE,
+    CONF_DEVICE_ID,
+    CONF_DEVICES,
+    CONF_HOST,
+    CONF_PORT,
+    CONF_TYPE,
+)
+from homeassistant.core import callback
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.device_registry import (
+    async_entries_for_config_entry,
+    async_get_registry as async_get_device_registry,
+)
+from homeassistant.helpers.entity_registry import (
+    async_entries_for_device,
+    async_get_registry as async_get_entity_registry,
+)
+
+from . import DOMAIN, get_device_id, get_rfx_object
+from .binary_sensor import supported as binary_supported
+from .const import (
+    CONF_AUTOMATIC_ADD,
+    CONF_DATA_BITS,
+    CONF_FIRE_EVENT,
+    CONF_OFF_DELAY,
+    CONF_REMOVE_DEVICE,
+    CONF_REPLACE_DEVICE,
+    CONF_SIGNAL_REPETITIONS,
+    DEVICE_PACKET_TYPE_LIGHTING4,
+)
+from .cover import supported as cover_supported
+from .light import supported as light_supported
+from .switch import supported as switch_supported
 
 _LOGGER = logging.getLogger(__name__)
 
+CONF_EVENT_CODE = "event_code"
+CONF_MANUAL_PATH = "Enter Manually"
+
+
+def none_or_int(value, base):
+    """Check if strin is one otherwise convert to int."""
+    if value is None:
+        return None
+    return int(value, base)
+
+
+class OptionsFlow(config_entries.OptionsFlow):
+    """Handle Rfxtrx options."""
+
+    def __init__(self, config_entry: ConfigEntry) -> None:
+        """Initialize rfxtrx options flow."""
+        self._config_entry = config_entry
+        self._global_options = None
+        self._selected_device = None
+        self._selected_device_entry_id = None
+        self._selected_device_event_code = None
+        self._selected_device_object = None
+        self._device_entries = None
+        self._device_registry = None
+
+    async def async_step_init(self, user_input=None):
+        """Manage the options."""
+        return await self.async_step_prompt_options()
+
+    async def async_step_prompt_options(self, user_input=None):
+        """Prompt for options."""
+        errors = {}
+
+        if user_input is not None:
+            self._global_options = {
+                CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD],
+            }
+            if CONF_DEVICE in user_input:
+                entry_id = user_input[CONF_DEVICE]
+                device_data = self._get_device_data(entry_id)
+                self._selected_device_entry_id = entry_id
+                event_code = device_data[CONF_EVENT_CODE]
+                self._selected_device_event_code = event_code
+                self._selected_device = self._config_entry.data[CONF_DEVICES][
+                    event_code
+                ]
+                self._selected_device_object = get_rfx_object(event_code)
+                return await self.async_step_set_device_options()
+            if CONF_REMOVE_DEVICE in user_input:
+                remove_devices = user_input[CONF_REMOVE_DEVICE]
+                devices = {}
+                for entry_id in remove_devices:
+                    device_data = self._get_device_data(entry_id)
+
+                    event_code = device_data[CONF_EVENT_CODE]
+                    device_id = device_data[CONF_DEVICE_ID]
+                    self.hass.helpers.dispatcher.async_dispatcher_send(
+                        f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{device_id}"
+                    )
+                    self._device_registry.async_remove_device(entry_id)
+                    devices[event_code] = None
+
+                self.update_config_data(
+                    global_options=self._global_options, devices=devices
+                )
+
+                return self.async_create_entry(title="", data={})
+            if CONF_EVENT_CODE in user_input:
+                self._selected_device_event_code = user_input[CONF_EVENT_CODE]
+                self._selected_device = {}
+                selected_device_object = get_rfx_object(
+                    self._selected_device_event_code
+                )
+                if selected_device_object is None:
+                    errors[CONF_EVENT_CODE] = "invalid_event_code"
+                elif not self._can_add_device(selected_device_object):
+                    errors[CONF_EVENT_CODE] = "already_configured_device"
+                else:
+                    self._selected_device_object = selected_device_object
+                    return await self.async_step_set_device_options()
+
+            if not errors:
+                self.update_config_data(global_options=self._global_options)
+
+                return self.async_create_entry(title="", data={})
+
+        device_registry = await async_get_device_registry(self.hass)
+        device_entries = async_entries_for_config_entry(
+            device_registry, self._config_entry.entry_id
+        )
+        self._device_registry = device_registry
+        self._device_entries = device_entries
+
+        devices = {
+            entry.id: entry.name_by_user if entry.name_by_user else entry.name
+            for entry in device_entries
+        }
+
+        options = {
+            vol.Optional(
+                CONF_AUTOMATIC_ADD,
+                default=self._config_entry.data[CONF_AUTOMATIC_ADD],
+            ): bool,
+            vol.Optional(CONF_EVENT_CODE): str,
+            vol.Optional(CONF_DEVICE): vol.In(devices),
+            vol.Optional(CONF_REMOVE_DEVICE): cv.multi_select(devices),
+        }
+
+        return self.async_show_form(
+            step_id="prompt_options", data_schema=vol.Schema(options), errors=errors
+        )
+
+    async def async_step_set_device_options(self, user_input=None):
+        """Manage device options."""
+        errors = {}
+
+        if user_input is not None:
+            device_id = get_device_id(
+                self._selected_device_object.device,
+                data_bits=user_input.get(CONF_DATA_BITS),
+            )
+
+            if CONF_REPLACE_DEVICE in user_input:
+                await self._async_replace_device(user_input[CONF_REPLACE_DEVICE])
+
+                devices = {self._selected_device_event_code: None}
+                self.update_config_data(
+                    global_options=self._global_options, devices=devices
+                )
+
+                return self.async_create_entry(title="", data={})
+
+            try:
+                command_on = none_or_int(user_input.get(CONF_COMMAND_ON), 16)
+            except ValueError:
+                errors[CONF_COMMAND_ON] = "invalid_input_2262_on"
+
+            try:
+                command_off = none_or_int(user_input.get(CONF_COMMAND_OFF), 16)
+            except ValueError:
+                errors[CONF_COMMAND_OFF] = "invalid_input_2262_off"
+
+            try:
+                off_delay = none_or_int(user_input.get(CONF_OFF_DELAY), 10)
+            except ValueError:
+                errors[CONF_OFF_DELAY] = "invalid_input_off_delay"
+
+            if not errors:
+                devices = {}
+                device = {
+                    CONF_DEVICE_ID: device_id,
+                    CONF_FIRE_EVENT: user_input.get(CONF_FIRE_EVENT, False),
+                    CONF_SIGNAL_REPETITIONS: user_input.get(CONF_SIGNAL_REPETITIONS, 1),
+                }
+
+                devices[self._selected_device_event_code] = device
+
+                if off_delay:
+                    device[CONF_OFF_DELAY] = off_delay
+                if user_input.get(CONF_DATA_BITS):
+                    device[CONF_DATA_BITS] = user_input[CONF_DATA_BITS]
+                if command_on:
+                    device[CONF_COMMAND_ON] = command_on
+                if command_off:
+                    device[CONF_COMMAND_OFF] = command_off
+
+                self.update_config_data(
+                    global_options=self._global_options, devices=devices
+                )
+
+                return self.async_create_entry(title="", data={})
+
+        device_data = self._selected_device
+
+        data_schema = {
+            vol.Optional(
+                CONF_FIRE_EVENT, default=device_data.get(CONF_FIRE_EVENT, False)
+            ): bool,
+        }
+
+        if binary_supported(self._selected_device_object):
+            if device_data.get(CONF_OFF_DELAY):
+                off_delay_schema = {
+                    vol.Optional(
+                        CONF_OFF_DELAY,
+                        description={"suggested_value": device_data[CONF_OFF_DELAY]},
+                    ): str,
+                }
+            else:
+                off_delay_schema = {
+                    vol.Optional(CONF_OFF_DELAY): str,
+                }
+            data_schema.update(off_delay_schema)
+
+        if (
+            binary_supported(self._selected_device_object)
+            or cover_supported(self._selected_device_object)
+            or light_supported(self._selected_device_object)
+            or switch_supported(self._selected_device_object)
+        ):
+            data_schema.update(
+                {
+                    vol.Optional(
+                        CONF_SIGNAL_REPETITIONS,
+                        default=device_data.get(CONF_SIGNAL_REPETITIONS, 1),
+                    ): int,
+                }
+            )
+
+        if (
+            self._selected_device_object.device.packettype
+            == DEVICE_PACKET_TYPE_LIGHTING4
+        ):
+            data_schema.update(
+                {
+                    vol.Optional(
+                        CONF_DATA_BITS, default=device_data.get(CONF_DATA_BITS, 0)
+                    ): int,
+                    vol.Optional(
+                        CONF_COMMAND_ON,
+                        default=hex(device_data.get(CONF_COMMAND_ON, 0)),
+                    ): str,
+                    vol.Optional(
+                        CONF_COMMAND_OFF,
+                        default=hex(device_data.get(CONF_COMMAND_OFF, 0)),
+                    ): str,
+                }
+            )
+
+        devices = {
+            entry.id: entry.name_by_user if entry.name_by_user else entry.name
+            for entry in self._device_entries
+            if self._can_replace_device(entry.id)
+        }
+
+        if devices:
+            data_schema.update(
+                {
+                    vol.Optional(CONF_REPLACE_DEVICE): vol.In(devices),
+                }
+            )
+
+        return self.async_show_form(
+            step_id="set_device_options",
+            data_schema=vol.Schema(data_schema),
+            errors=errors,
+        )
+
+    async def _async_replace_device(self, replace_device):
+        """Migrate properties of a device into another."""
+        device_registry = self._device_registry
+        old_device = self._selected_device_entry_id
+        old_entry = device_registry.async_get(old_device)
+        device_registry.async_update_device(
+            replace_device,
+            area_id=old_entry.area_id,
+            name_by_user=old_entry.name_by_user,
+        )
+
+        old_device_data = self._get_device_data(old_device)
+        new_device_data = self._get_device_data(replace_device)
+
+        old_device_id = "_".join(x for x in old_device_data[CONF_DEVICE_ID])
+        new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID])
+
+        entity_registry = await async_get_entity_registry(self.hass)
+        entity_entries = async_entries_for_device(entity_registry, old_device)
+        entity_migration_map = {}
+        for entry in entity_entries:
+            unique_id = entry.unique_id
+            new_unique_id = unique_id.replace(old_device_id, new_device_id)
+
+            new_entity_id = entity_registry.async_get_entity_id(
+                entry.domain, entry.platform, new_unique_id
+            )
+
+            if new_entity_id is not None:
+                entity_migration_map[new_entity_id] = entry
+
+        for entry in entity_migration_map.values():
+            entity_registry.async_remove(entry.entity_id)
+
+        for entity_id, entry in entity_migration_map.items():
+            entity_registry.async_update_entity(
+                entity_id,
+                new_entity_id=entry.entity_id,
+                name=entry.name,
+                icon=entry.icon,
+            )
+
+        device_registry.async_remove_device(old_device)
+
+    def _can_add_device(self, new_rfx_obj):
+        """Check if device does not already exist."""
+        new_device_id = get_device_id(new_rfx_obj.device)
+        for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items():
+            rfx_obj = get_rfx_object(packet_id)
+            device_id = get_device_id(rfx_obj.device, entity_info.get(CONF_DATA_BITS))
+            if new_device_id == device_id:
+                return False
+
+        return True
+
+    def _can_replace_device(self, entry_id):
+        """Check if device can be replaced with selected device."""
+        device_data = self._get_device_data(entry_id)
+        event_code = device_data[CONF_EVENT_CODE]
+        rfx_obj = get_rfx_object(event_code)
+        if (
+            rfx_obj.device.packettype == self._selected_device_object.device.packettype
+            and rfx_obj.device.subtype == self._selected_device_object.device.subtype
+            and self._selected_device_event_code != event_code
+        ):
+            return True
+
+        return False
+
+    def _get_device_data(self, entry_id):
+        """Get event code based on device identifier."""
+        event_code = None
+        device_id = None
+        entry = self._device_registry.async_get(entry_id)
+        device_id = next(iter(entry.identifiers))[1:]
+        for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items():
+            if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id:
+                event_code = packet_id
+                break
+
+        data = {CONF_EVENT_CODE: event_code, CONF_DEVICE_ID: device_id}
+
+        return data
+
+    @callback
+    def update_config_data(self, global_options=None, devices=None):
+        """Update data in ConfigEntry."""
+        entry_data = self._config_entry.data.copy()
+        entry_data[CONF_DEVICES] = copy.deepcopy(self._config_entry.data[CONF_DEVICES])
+        if global_options:
+            entry_data.update(global_options)
+        if devices:
+            for event_code, options in devices.items():
+                if options is None:
+                    entry_data[CONF_DEVICES].pop(event_code)
+                else:
+                    entry_data[CONF_DEVICES][event_code] = options
+        self.hass.config_entries.async_update_entry(self._config_entry, data=entry_data)
+        self.hass.async_create_task(
+            self.hass.config_entries.async_reload(self._config_entry.entry_id)
+        )
+
 
 class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
     """Handle a config flow for RFXCOM RFXtrx."""
@@ -14,11 +406,190 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
     VERSION = 1
     CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
 
+    async def async_step_user(self, user_input=None):
+        """Step when user initializes a integration."""
+        await self.async_set_unique_id(DOMAIN)
+        self._abort_if_unique_id_configured()
+
+        errors = {}
+        if user_input is not None:
+            user_selection = user_input[CONF_TYPE]
+            if user_selection == "Serial":
+                return await self.async_step_setup_serial()
+
+            return await self.async_step_setup_network()
+
+        list_of_types = ["Serial", "Network"]
+
+        schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)})
+        return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+    async def async_step_setup_network(self, user_input=None):
+        """Step when setting up network configuration."""
+        errors = {}
+
+        if user_input is not None:
+            host = user_input[CONF_HOST]
+            port = user_input[CONF_PORT]
+
+            try:
+                data = await self.async_validate_rfx(host=host, port=port)
+            except CannotConnect:
+                errors["base"] = "cannot_connect"
+
+            if not errors:
+                return self.async_create_entry(title="RFXTRX", data=data)
+
+        schema = vol.Schema(
+            {vol.Required(CONF_HOST): str, vol.Required(CONF_PORT): int}
+        )
+        return self.async_show_form(
+            step_id="setup_network",
+            data_schema=schema,
+            errors=errors,
+        )
+
+    async def async_step_setup_serial(self, user_input=None):
+        """Step when setting up serial configuration."""
+        errors = {}
+
+        if user_input is not None:
+            user_selection = user_input[CONF_DEVICE]
+            if user_selection == CONF_MANUAL_PATH:
+                return await self.async_step_setup_serial_manual_path()
+
+            dev_path = await self.hass.async_add_executor_job(
+                get_serial_by_id, user_selection
+            )
+
+            try:
+                data = await self.async_validate_rfx(device=dev_path)
+            except CannotConnect:
+                errors["base"] = "cannot_connect"
+
+            if not errors:
+                return self.async_create_entry(title="RFXTRX", data=data)
+
+        ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
+        list_of_ports = {}
+        for port in ports:
+            list_of_ports[
+                port.device
+            ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + (
+                f" - {port.manufacturer}" if port.manufacturer else ""
+            )
+        list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
+
+        schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(list_of_ports)})
+        return self.async_show_form(
+            step_id="setup_serial",
+            data_schema=schema,
+            errors=errors,
+        )
+
+    async def async_step_setup_serial_manual_path(self, user_input=None):
+        """Select path manually."""
+        errors = {}
+
+        if user_input is not None:
+            device = user_input[CONF_DEVICE]
+            try:
+                data = await self.async_validate_rfx(device=device)
+            except CannotConnect:
+                errors["base"] = "cannot_connect"
+
+            if not errors:
+                return self.async_create_entry(title="RFXTRX", data=data)
+
+        schema = vol.Schema({vol.Required(CONF_DEVICE): str})
+        return self.async_show_form(
+            step_id="setup_serial_manual_path",
+            data_schema=schema,
+            errors=errors,
+        )
+
     async def async_step_import(self, import_config=None):
         """Handle the initial step."""
         entry = await self.async_set_unique_id(DOMAIN)
-        if entry and import_config.items() != entry.data.items():
-            self.hass.config_entries.async_update_entry(entry, data=import_config)
-            return self.async_abort(reason="already_configured")
-        self._abort_if_unique_id_configured()
+        if entry:
+            if CONF_DEVICES not in entry.data:
+                # In version 0.113, devices key was not written to config entry. Update the entry with import data
+                self._abort_if_unique_id_configured(import_config)
+            else:
+                self._abort_if_unique_id_configured()
+
+        host = import_config[CONF_HOST]
+        port = import_config[CONF_PORT]
+        device = import_config[CONF_DEVICE]
+
+        try:
+            if host is not None:
+                await self.async_validate_rfx(host=host, port=port)
+            else:
+                await self.async_validate_rfx(device=device)
+        except CannotConnect:
+            return self.async_abort(reason="cannot_connect")
+
         return self.async_create_entry(title="RFXTRX", data=import_config)
+
+    async def async_validate_rfx(self, host=None, port=None, device=None):
+        """Create data for rfxtrx entry."""
+        success = await self.hass.async_add_executor_job(
+            _test_transport, host, port, device
+        )
+        if not success:
+            raise CannotConnect
+
+        data = {
+            CONF_HOST: host,
+            CONF_PORT: port,
+            CONF_DEVICE: device,
+            CONF_AUTOMATIC_ADD: False,
+            CONF_DEVICES: {},
+        }
+        return data
+
+    @staticmethod
+    @callback
+    def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
+        """Get the options flow for this handler."""
+        return OptionsFlow(config_entry)
+
+
+def _test_transport(host, port, device):
+    """Construct a rfx object based on config."""
+    if port is not None:
+        try:
+            conn = rfxtrxmod.PyNetworkTransport((host, port))
+        except OSError:
+            return False
+
+        conn.close()
+    else:
+        try:
+            conn = rfxtrxmod.PySerialTransport(device)
+        except serial.serialutil.SerialException:
+            return False
+
+        if conn.serial is None:
+            return False
+
+        conn.close()
+
+    return True
+
+
+def get_serial_by_id(dev_path: str) -> str:
+    """Return a /dev/serial/by-id match for given device if available."""
+    by_id = "/dev/serial/by-id"
+    if not os.path.isdir(by_id):
+        return dev_path
+
+    for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
+        if os.path.realpath(path) == dev_path:
+            return path
+    return dev_path
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+    """Error to indicate we cannot connect."""
diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py
index c0436bfcf60..404d344cc71 100644
--- a/homeassistant/components/rfxtrx/const.py
+++ b/homeassistant/components/rfxtrx/const.py
@@ -1,5 +1,14 @@
 """Constants for RFXtrx integration."""
 
+CONF_FIRE_EVENT = "fire_event"
+CONF_DATA_BITS = "data_bits"
+CONF_AUTOMATIC_ADD = "automatic_add"
+CONF_SIGNAL_REPETITIONS = "signal_repetitions"
+CONF_DEBUG = "debug"
+CONF_OFF_DELAY = "off_delay"
+
+CONF_REMOVE_DEVICE = "remove_device"
+CONF_REPLACE_DEVICE = "replace_device"
 
 COMMAND_ON_LIST = [
     "On",
diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py
index fc6ab6cbf15..86950308f55 100644
--- a/homeassistant/components/rfxtrx/cover.py
+++ b/homeassistant/components/rfxtrx/cover.py
@@ -20,6 +20,11 @@ from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST
 _LOGGER = logging.getLogger(__name__)
 
 
+def supported(event):
+    """Return whether an event supports cover."""
+    return event.device.known_to_be_rollershutter
+
+
 async def async_setup_entry(
     hass,
     config_entry,
@@ -29,9 +34,6 @@ async def async_setup_entry(
     discovery_info = config_entry.data
     device_ids = set()
 
-    def supported(event):
-        return event.device.known_to_be_rollershutter
-
     entities = []
     for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
         event = get_rfx_object(packet_id)
diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py
index 791cc158693..33ee5ea4748 100644
--- a/homeassistant/components/rfxtrx/light.py
+++ b/homeassistant/components/rfxtrx/light.py
@@ -28,6 +28,14 @@ _LOGGER = logging.getLogger(__name__)
 SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS
 
 
+def supported(event):
+    """Return whether an event supports light."""
+    return (
+        isinstance(event.device, rfxtrxmod.LightingDevice)
+        and event.device.known_to_be_dimmable
+    )
+
+
 async def async_setup_entry(
     hass,
     config_entry,
@@ -37,12 +45,6 @@ async def async_setup_entry(
     discovery_info = config_entry.data
     device_ids = set()
 
-    def supported(event):
-        return (
-            isinstance(event.device, rfxtrxmod.LightingDevice)
-            and event.device.known_to_be_dimmable
-        )
-
     # Add switch from config file
     entities = []
     for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json
index 44b53ed0dac..e62fc5c3c83 100644
--- a/homeassistant/components/rfxtrx/manifest.json
+++ b/homeassistant/components/rfxtrx/manifest.json
@@ -2,7 +2,7 @@
   "domain": "rfxtrx",
   "name": "RFXCOM RFXtrx",
   "documentation": "https://www.home-assistant.io/integrations/rfxtrx",
-  "requirements": ["pyRFXtrx==0.25"],
-  "codeowners": ["@danielhiversen", "@elupus"],
-  "config_flow": false
-}
\ No newline at end of file
+  "requirements": ["pyRFXtrx==0.26"],
+  "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
+  "config_flow": true
+}
diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json
index e19265dec32..9e976999157 100644
--- a/homeassistant/components/rfxtrx/strings.json
+++ b/homeassistant/components/rfxtrx/strings.json
@@ -1,9 +1,74 @@
 {
-    "config": {
-        "step": {},
-        "error": {},
-        "abort": {
-            "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
-        }
+  "title": "Rfxtrx",
+  "config": {
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+    },
+    "step": {
+      "user": {
+        "data": {
+          "type": "Connection type"
+        },
+        "title": "Select connection type"
+      },
+      "setup_network": {
+        "data": {
+          "host": "[%key:common::config_flow::data::host%]",
+          "port": "[%key:common::config_flow::data::port%]"
+        },
+        "title": "Select connection address"
+      },
+      "setup_serial": {
+        "data": {
+          "device": "Select device"
+        },
+        "title": "Device"
+      },
+      "setup_serial_manual_path": {
+        "data": {
+          "device": "[%key:common::config_flow::data::usb_path%]"
+        },
+        "title": "Path"
+      }
     }
+  },
+  "options": {
+    "step": {
+      "prompt_options": {
+        "data": {
+          "debug": "Enable debugging",
+          "automatic_add": "Enable automatic add",
+          "event_code": "Enter event code to add",
+          "device": "Select device to configure",
+          "remove_device": "Select device to delete"
+        },
+        "title": "Rfxtrx Options"
+      },
+      "set_device_options": {
+        "data": {
+          "fire_event": "Enable device event",
+          "off_delay": "Off delay",
+          "off_delay_enabled": "Enable off delay",
+          "data_bit": "Number of data bits",
+          "command_on": "Data bits value for command on",
+          "command_off": "Data bits value for command off",
+          "signal_repetitions": "Number of signal repetitions",
+          "replace_device": "Select device to replace"
+        },
+        "title": "Configure device options"
+      }
+    },
+    "error": {
+      "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
+      "invalid_event_code": "Invalid event code",
+      "invalid_input_2262_on": "Invalid input for command on",
+      "invalid_input_2262_off": "Invalid input for command off",
+      "invalid_input_off_delay": "Invalid input for off delay",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    }
+  }
 }
diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py
index bce5222b778..53069210794 100644
--- a/homeassistant/components/rfxtrx/switch.py
+++ b/homeassistant/components/rfxtrx/switch.py
@@ -25,6 +25,16 @@ DATA_SWITCH = f"{DOMAIN}_switch"
 _LOGGER = logging.getLogger(__name__)
 
 
+def supported(event):
+    """Return whether an event supports switch."""
+    return (
+        isinstance(event.device, rfxtrxmod.LightingDevice)
+        and not event.device.known_to_be_dimmable
+        and not event.device.known_to_be_rollershutter
+        or isinstance(event.device, rfxtrxmod.RfyDevice)
+    )
+
+
 async def async_setup_entry(
     hass,
     config_entry,
@@ -34,14 +44,6 @@ async def async_setup_entry(
     discovery_info = config_entry.data
     device_ids = set()
 
-    def supported(event):
-        return (
-            isinstance(event.device, rfxtrxmod.LightingDevice)
-            and not event.device.known_to_be_dimmable
-            and not event.device.known_to_be_rollershutter
-            or isinstance(event.device, rfxtrxmod.RfyDevice)
-        )
-
     # Add switch from config file
     entities = []
     for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json
index 1344d2f6988..ebb7c77f303 100644
--- a/homeassistant/components/rfxtrx/translations/en.json
+++ b/homeassistant/components/rfxtrx/translations/en.json
@@ -1,7 +1,74 @@
 {
     "config": {
         "abort": {
-            "already_configured": "Device is already configured"
+            "already_configured": "Already configured. Only a single configuration possible.",
+            "cannot_connect": "Failed to connect"
+        },
+        "error": {
+            "cannot_connect": "Failed to connect"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "type": "Connection type"
+                },
+                "title": "Select connection type"
+            },
+            "setup_network": {
+                "data": {
+                    "host": "Host",
+                    "port": "Port"
+                },
+                "title": "Select connection address"
+            },
+            "setup_serial": {
+                "data": {
+                    "device": "Select device"
+                },
+                "title": "Device"
+            },
+            "setup_serial_manual_path": {
+                "data": {
+                    "device": "Select path"
+                },
+                "title": "Path"
+            }
         }
-    }
-}
\ No newline at end of file
+    },
+    "options": {
+        "step": {
+            "prompt_options": {
+                "data": {
+                    "debug": "Enable debugging",
+                    "automatic_add": "Enable automatic add",
+                    "event_code": "Enter event code to add",
+                    "device": "Select device to configure",
+                    "remove_device": "Select device to delete"
+                },
+                "title": "Rfxtrx Options"
+            },
+            "set_device_options": {
+                "data": {
+                    "fire_event": "Enable device event",
+                    "off_delay": "Off delay",
+                    "off_delay_enabled": "Enable off delay",
+                    "data_bit": "Number of data bits",
+                    "command_on": "Data bits value for command on",
+                    "command_off": "Data bits value for command off",
+                    "signal_repetitions": "Number of signal repetitions",
+                    "replace_device": "Select device to replace"
+                },
+                "title": "Configure device options"
+            }
+        },
+        "error": {
+            "already_configured_device": "Device is already configured",
+            "invalid_event_code": "Invalid event code",
+            "invalid_input_2262_on": "Invalid input for command on",
+            "invalid_input_2262_off": "Invalid input for command off",
+            "invalid_input_off_delay": "Invalid input for off delay",
+            "unknown": "Unexpected error"
+        }
+    },
+    "title": "Rfxtrx"
+}
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index bfd3c340e6d..0347b8c82d6 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -150,6 +150,7 @@ FLOWS = [
     "pvpc_hourly_pricing",
     "rachio",
     "rainmachine",
+    "rfxtrx",
     "ring",
     "risco",
     "roku",
diff --git a/requirements_all.txt b/requirements_all.txt
index 39d606181e3..0ad24c0ae88 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1211,7 +1211,7 @@ pyHS100==0.3.5.1
 pyMetno==0.8.1
 
 # homeassistant.components.rfxtrx
-pyRFXtrx==0.25
+pyRFXtrx==0.26
 
 # homeassistant.components.switchmate
 # pySwitchmate==0.4.6
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f28ba1c4b49..86b1faf8965 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -592,7 +592,7 @@ pyHS100==0.3.5.1
 pyMetno==0.8.1
 
 # homeassistant.components.rfxtrx
-pyRFXtrx==0.25
+pyRFXtrx==0.26
 
 # homeassistant.components.tibber
 pyTibber==0.15.3
diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py
index 1eb39f00691..82c4bd7aacd 100644
--- a/tests/components/rfxtrx/conftest.py
+++ b/tests/components/rfxtrx/conftest.py
@@ -4,11 +4,23 @@ from datetime import timedelta
 import pytest
 
 from homeassistant.components import rfxtrx
-from homeassistant.setup import async_setup_component
+from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.util.dt import utcnow
 
 from tests.async_mock import patch
-from tests.common import async_fire_time_changed
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None):
+    """Create rfxtrx config entry data."""
+    return {
+        "device": device,
+        "host": None,
+        "port": None,
+        "automatic_add": automatic_add,
+        "debug": False,
+        "devices": devices,
+    }
 
 
 @pytest.fixture(autouse=True, name="rfxtrx")
@@ -37,12 +49,12 @@ async def rfxtrx_fixture(hass):
 @pytest.fixture(name="rfxtrx_automatic")
 async def rfxtrx_automatic_fixture(hass, rfxtrx):
     """Fixture that starts up with automatic additions."""
+    entry_data = create_rfx_test_cfg(automatic_add=True, devices={})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "automatic_add": True}},
-    )
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
     yield rfxtrx
diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py
index ee757192aaf..a52b390395a 100644
--- a/tests/components/rfxtrx/test_binary_sensor.py
+++ b/tests/components/rfxtrx/test_binary_sensor.py
@@ -1,11 +1,12 @@
 """The tests for the Rfxtrx sensor platform."""
 import pytest
 
+from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.components.rfxtrx.const import ATTR_EVENT
 from homeassistant.core import State
-from homeassistant.setup import async_setup_component
 
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
 
 EVENT_SMOKE_DETECTOR_PANIC = "08200300a109000670"
 EVENT_SMOKE_DETECTOR_NO_PANIC = "08200300a109000770"
@@ -21,11 +22,12 @@ EVENT_AC_118CDEA_2_ON = "0b1100100118cdea02010f70"
 
 async def test_one(hass, rfxtrx):
     """Test with 1 sensor."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}},
-    )
+    entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("binary_sensor.ac_213c7f2_48")
@@ -36,22 +38,20 @@ async def test_one(hass, rfxtrx):
 
 async def test_one_pt2262(hass, rfxtrx):
     """Test with 1 sensor."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0913000022670e013970": {
-                        "data_bits": 4,
-                        "command_on": 0xE,
-                        "command_off": 0x7,
-                    }
-                },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0913000022670e013970": {
+                "data_bits": 4,
+                "command_on": 0xE,
+                "command_off": 0x7,
             }
-        },
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -71,19 +71,14 @@ async def test_one_pt2262(hass, rfxtrx):
 
 async def test_pt2262_unconfigured(hass, rfxtrx):
     """Test with discovery for PT2262."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0913000022670e013970": {},
-                    "09130000226707013970": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={"0913000022670e013970": {}, "09130000226707013970": {}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -109,11 +104,12 @@ async def test_state_restore(hass, rfxtrx, state, event):
 
     mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})])
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}},
-    )
+    entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     assert hass.states.get(entity_id).state == state
@@ -121,20 +117,18 @@ async def test_state_restore(hass, rfxtrx, state, event):
 
 async def test_several(hass, rfxtrx):
     """Test with 3."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1100cd0213c7f230010f71": {},
-                    "0b1100100118cdea02010f70": {},
-                    "0b1100101118cdea02010f70": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0b1100cd0213c7f230010f71": {},
+            "0b1100100118cdea02010f70": {},
+            "0b1100101118cdea02010f70": {},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("binary_sensor.ac_213c7f2_48")
@@ -181,16 +175,12 @@ async def test_off_delay_restore(hass, rfxtrx):
         ],
     )
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {EVENT_AC_118CDEA_2_ON: {"off_delay": 5}},
-            }
-        },
-    )
+    entry_data = create_rfx_test_cfg(devices={EVENT_AC_118CDEA_2_ON: {"off_delay": 5}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -201,16 +191,14 @@ async def test_off_delay_restore(hass, rfxtrx):
 
 async def test_off_delay(hass, rfxtrx, timestep):
     """Test with discovery."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {"0b1100100118cdea02010f70": {"off_delay": 5}},
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100100118cdea02010f70": {"off_delay": 5}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -295,27 +283,25 @@ async def test_light(hass, rfxtrx_automatic):
 
 async def test_pt2262_duplicate_id(hass, rfxtrx):
     """Test with 1 sensor."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0913000022670e013970": {
-                        "data_bits": 4,
-                        "command_on": 0xE,
-                        "command_off": 0x7,
-                    },
-                    "09130000226707013970": {
-                        "data_bits": 4,
-                        "command_on": 0xE,
-                        "command_off": 0x7,
-                    },
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0913000022670e013970": {
+                "data_bits": 4,
+                "command_on": 0xE,
+                "command_off": 0x7,
+            },
+            "09130000226707013970": {
+                "data_bits": 4,
+                "command_on": 0xE,
+                "command_off": 0x7,
+            },
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py
index 53f3b317d53..6ba045d60a6 100644
--- a/tests/components/rfxtrx/test_config_flow.py
+++ b/tests/components/rfxtrx/test_config_flow.py
@@ -1,20 +1,307 @@
 """Test the Tado config flow."""
-from homeassistant import config_entries, setup
-from homeassistant.components.rfxtrx import DOMAIN
+import os
 
+import serial.tools.list_ports
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.rfxtrx import DOMAIN, config_flow
+from homeassistant.helpers.device_registry import (
+    async_entries_for_config_entry,
+    async_get_registry as async_get_device_registry,
+)
+from homeassistant.helpers.entity_registry import (
+    async_get_registry as async_get_entity_registry,
+)
+
+from tests.async_mock import MagicMock, patch, sentinel
 from tests.common import MockConfigEntry
 
 
-async def test_import(hass):
-    """Test we can import."""
-    await setup.async_setup_component(hass, "persistent_notification", {})
+def serial_connect(self):
+    """Mock a serial connection."""
+    self.serial = True
+
+
+def serial_connect_fail(self):
+    """Mock a failed serial connection."""
+    self.serial = None
 
+
+def com_port():
+    """Mock of a serial port."""
+    port = serial.tools.list_ports_common.ListPortInfo()
+    port.serial_number = "1234"
+    port.manufacturer = "Virtual serial port"
+    port.device = "/dev/ttyUSB1234"
+    port.description = "Some serial port"
+
+    return port
+
+
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+    return_value=None,
+)
+async def test_setup_network(connect_mock, hass):
+    """Test we can setup network."""
     result = await hass.config_entries.flow.async_init(
-        DOMAIN,
-        context={"source": config_entries.SOURCE_IMPORT},
-        data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "user"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {"type": "Network"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_network"
+    assert result["errors"] == {}
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"], {"host": "10.10.0.1", "port": 1234}
+        )
+
+    assert result["type"] == "create_entry"
+    assert result["title"] == "RFXTRX"
+    assert result["data"] == {
+        "host": "10.10.0.1",
+        "port": 1234,
+        "device": None,
+        "automatic_add": False,
+        "devices": {},
+    }
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+    serial_connect,
+)
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close",
+    return_value=None,
+)
+async def test_setup_serial(com_mock, connect_mock, hass):
+    """Test we can setup serial."""
+    port = com_port()
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "user"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {"type": "Serial"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial"
+    assert result["errors"] == {}
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"], {"device": port.device}
+        )
+
+    assert result["type"] == "create_entry"
+    assert result["title"] == "RFXTRX"
+    assert result["data"] == {
+        "host": None,
+        "port": None,
+        "device": port.device,
+        "automatic_add": False,
+        "devices": {},
+    }
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+    serial_connect,
+)
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close",
+    return_value=None,
+)
+async def test_setup_serial_manual(com_mock, connect_mock, hass):
+    """Test we can setup serial with manual entry."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "user"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {"type": "Serial"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"], {"device": "Enter Manually"}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial_manual_path"
+    assert result["errors"] == {}
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"], {"device": "/dev/ttyUSB0"}
+        )
+
+    assert result["type"] == "create_entry"
+    assert result["title"] == "RFXTRX"
+    assert result["data"] == {
+        "host": None,
+        "port": None,
+        "device": "/dev/ttyUSB0",
+        "automatic_add": False,
+        "devices": {},
+    }
+
+
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+    side_effect=OSError,
+)
+async def test_setup_network_fail(connect_mock, hass):
+    """Test we can setup network."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "user"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {"type": "Network"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_network"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"], {"host": "10.10.0.1", "port": 1234}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_network"
+    assert result["errors"] == {"base": "cannot_connect"}
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+    side_effect=serial.serialutil.SerialException,
+)
+async def test_setup_serial_fail(com_mock, connect_mock, hass):
+    """Test setup serial failed connection."""
+    port = com_port()
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "user"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {"type": "Serial"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"], {"device": port.device}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial"
+    assert result["errors"] == {"base": "cannot_connect"}
+
+
+@patch("serial.tools.list_ports.comports", return_value=[com_port()])
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+    serial_connect_fail,
+)
+async def test_setup_serial_manual_fail(com_mock, hass):
+    """Test setup serial failed connection."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "user"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {"type": "Serial"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"], {"device": "Enter Manually"}
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial_manual_path"
+    assert result["errors"] == {}
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"], {"device": "/dev/ttyUSB0"}
     )
 
+    assert result["type"] == "form"
+    assert result["step_id"] == "setup_serial_manual_path"
+    assert result["errors"] == {"base": "cannot_connect"}
+
+
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect",
+    serial_connect,
+)
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close",
+    return_value=None,
+)
+async def test_import_serial(connect_mock, hass):
+    """Test we can import."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+        )
+
     assert result["type"] == "create_entry"
     assert result["title"] == "RFXTRX"
     assert result["data"] == {
@@ -25,13 +312,63 @@ async def test_import(hass):
     }
 
 
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+    return_value=None,
+)
+async def test_import_network(connect_mock, hass):
+    """Test we can import."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data={"host": "localhost", "port": 1234, "device": None, "debug": False},
+        )
+
+    assert result["type"] == "create_entry"
+    assert result["title"] == "RFXTRX"
+    assert result["data"] == {
+        "host": "localhost",
+        "port": 1234,
+        "device": None,
+        "debug": False,
+    }
+
+
+@patch(
+    "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect",
+    side_effect=OSError,
+)
+async def test_import_network_connection_fail(connect_mock, hass):
+    """Test we can import."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data={"host": "localhost", "port": 1234, "device": None, "debug": False},
+        )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "cannot_connect"
+
+
 async def test_import_update(hass):
     """Test we can import."""
     await setup.async_setup_component(hass, "persistent_notification", {})
 
     entry = MockConfigEntry(
         domain=DOMAIN,
-        data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "debug": False,
+            "devices": {},
+        },
         unique_id=DOMAIN,
     )
     entry.add_to_hass(hass)
@@ -39,9 +376,775 @@ async def test_import_update(hass):
     result = await hass.config_entries.flow.async_init(
         DOMAIN,
         context={"source": config_entries.SOURCE_IMPORT},
-        data={"host": None, "port": None, "device": "/dev/tty123", "debug": True},
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "debug": True,
+            "devices": {},
+        },
     )
 
     assert result["type"] == "abort"
     assert result["reason"] == "already_configured"
-    assert entry.data["debug"]
+
+
+async def test_import_migrate(hass):
+    """Test we can import."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={"host": None, "port": None, "device": "/dev/tty123", "debug": False},
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data={
+                "host": None,
+                "port": None,
+                "device": "/dev/tty123",
+                "debug": True,
+                "automatic_add": True,
+                "devices": {},
+            },
+        )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "already_configured"
+
+    assert entry.data["devices"] == {}
+
+
+async def test_options_global(hass):
+    """Test if we can set global options."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {},
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"], user_input={"automatic_add": True}
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    assert entry.data["automatic_add"]
+
+
+async def test_options_add_device(hass):
+    """Test we can add a device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {},
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    # Try with invalid event code
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={"automatic_add": True, "event_code": "1234"},
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+    assert result["errors"]
+    assert result["errors"]["event_code"] == "invalid_event_code"
+
+    # Try with valid event code
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": True,
+            "event_code": "0b1100cd0213c7f230010f71",
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"], user_input={"fire_event": True, "signal_repetitions": 5}
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    assert entry.data["automatic_add"]
+
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"]
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5
+    assert "delay_off" not in entry.data["devices"]["0b1100cd0213c7f230010f71"]
+
+    state = hass.states.get("binary_sensor.ac_213c7f2_48")
+    assert state
+    assert state.state == "off"
+    assert state.attributes.get("friendly_name") == "AC 213c7f2:48"
+
+
+async def test_options_add_duplicate_device(hass):
+    """Test we can add a device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "debug": False,
+            "automatic_add": False,
+            "devices": {"0b1100cd0213c7f230010f71": {"signal_repetitions": 1}},
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": True,
+            "event_code": "0b1100cd0213c7f230010f71",
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+    assert result["errors"]
+    assert result["errors"]["event_code"] == "already_configured_device"
+
+
+async def test_options_add_remove_device(hass):
+    """Test we can add a device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {},
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": True,
+            "event_code": "0b1100cd0213c7f230010f71",
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={"fire_event": True, "signal_repetitions": 5, "off_delay": "4"},
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    assert entry.data["automatic_add"]
+
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"]
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5
+    assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["off_delay"] == 4
+
+    state = hass.states.get("binary_sensor.ac_213c7f2_48")
+    assert state
+    assert state.state == "off"
+    assert state.attributes.get("friendly_name") == "AC 213c7f2:48"
+
+    device_registry = await async_get_device_registry(hass)
+    device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+    assert device_entries[0].id
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": False,
+            "remove_device": [device_entries[0].id],
+        },
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    assert not entry.data["automatic_add"]
+
+    assert "0b1100cd0213c7f230010f71" not in entry.data["devices"]
+
+    state = hass.states.get("binary_sensor.ac_213c7f2_48")
+    assert not state
+
+
+async def test_options_replace_sensor_device(hass):
+    """Test we can replace a sensor device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {
+                "0a520101f00400e22d0189": {"device_id": ["52", "1", "f0:04"]},
+                "0a520105230400c3260279": {"device_id": ["52", "1", "23:04"]},
+            },
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity_status"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_temperature"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity_status"
+    )
+    assert state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_temperature"
+    )
+    assert state
+
+    device_registry = await async_get_device_registry(hass)
+    device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+    old_device = next(
+        (
+            elem.id
+            for elem in device_entries
+            if next(iter(elem.identifiers))[1:] == ("52", "1", "f0:04")
+        ),
+        None,
+    )
+    new_device = next(
+        (
+            elem.id
+            for elem in device_entries
+            if next(iter(elem.identifiers))[1:] == ("52", "1", "23:04")
+        ),
+        None,
+    )
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": False,
+            "device": old_device,
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "replace_device": new_device,
+        },
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    entity_registry = await async_get_entity_registry(hass)
+
+    entry = entity_registry.async_get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric"
+    )
+    assert entry
+    assert entry.device_id == new_device
+    entry = entity_registry.async_get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity"
+    )
+    assert entry
+    assert entry.device_id == new_device
+    entry = entity_registry.async_get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity_status"
+    )
+    assert entry
+    assert entry.device_id == new_device
+    entry = entity_registry.async_get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric"
+    )
+    assert entry
+    assert entry.device_id == new_device
+    entry = entity_registry.async_get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_temperature"
+    )
+    assert entry
+    assert entry.device_id == new_device
+
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric"
+    )
+    assert not state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric"
+    )
+    assert not state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity"
+    )
+    assert not state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity_status"
+    )
+    assert not state
+    state = hass.states.get(
+        "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_temperature"
+    )
+    assert not state
+
+
+async def test_options_replace_control_device(hass):
+    """Test we can replace a control device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {
+                "0b1100100118cdea02010f70": {
+                    "device_id": ["11", "0", "118cdea:2"],
+                    "signal_repetitions": 1,
+                },
+                "0b1100101118cdea02010f70": {
+                    "device_id": ["11", "0", "1118cdea:2"],
+                    "signal_repetitions": 1,
+                },
+            },
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    state = hass.states.get("binary_sensor.ac_118cdea_2")
+    assert state
+    state = hass.states.get("sensor.ac_118cdea_2_rssi_numeric")
+    assert state
+    state = hass.states.get("switch.ac_118cdea_2")
+    assert state
+    state = hass.states.get("binary_sensor.ac_1118cdea_2")
+    assert state
+    state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric")
+    assert state
+    state = hass.states.get("switch.ac_1118cdea_2")
+    assert state
+
+    device_registry = await async_get_device_registry(hass)
+    device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+    old_device = next(
+        (
+            elem.id
+            for elem in device_entries
+            if next(iter(elem.identifiers))[1:] == ("11", "0", "118cdea:2")
+        ),
+        None,
+    )
+    new_device = next(
+        (
+            elem.id
+            for elem in device_entries
+            if next(iter(elem.identifiers))[1:] == ("11", "0", "1118cdea:2")
+        ),
+        None,
+    )
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": False,
+            "device": old_device,
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "replace_device": new_device,
+        },
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    entity_registry = await async_get_entity_registry(hass)
+
+    entry = entity_registry.async_get("binary_sensor.ac_118cdea_2")
+    assert entry
+    assert entry.device_id == new_device
+    entry = entity_registry.async_get("sensor.ac_118cdea_2_rssi_numeric")
+    assert entry
+    assert entry.device_id == new_device
+    entry = entity_registry.async_get("switch.ac_118cdea_2")
+    assert entry
+    assert entry.device_id == new_device
+
+    state = hass.states.get("binary_sensor.ac_1118cdea_2")
+    assert not state
+    state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric")
+    assert not state
+    state = hass.states.get("switch.ac_1118cdea_2")
+    assert not state
+
+
+async def test_options_remove_multiple_devices(hass):
+    """Test we can add a device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {
+                "0b1100cd0213c7f230010f71": {"device_id": ["11", "0", "213c7f2:48"]},
+                "0b1100100118cdea02010f70": {"device_id": ["11", "0", "118cdea:2"]},
+                "0b1100101118cdea02010f70": {"device_id": ["11", "0", "1118cdea:2"]},
+            },
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    state = hass.states.get("binary_sensor.ac_213c7f2_48")
+    assert state
+    state = hass.states.get("binary_sensor.ac_118cdea_2")
+    assert state
+    state = hass.states.get("binary_sensor.ac_1118cdea_2")
+    assert state
+
+    device_registry = await async_get_device_registry(hass)
+    device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+    assert len(device_entries) == 3
+
+    def match_device_id(entry):
+        device_id = next(iter(entry.identifiers))[1:]
+        if device_id == ("11", "0", "213c7f2:48"):
+            return True
+        if device_id == ("11", "0", "118cdea:2"):
+            return True
+        return False
+
+    remove_devices = [elem.id for elem in device_entries if match_device_id(elem)]
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": False,
+            "remove_device": remove_devices,
+        },
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    state = hass.states.get("binary_sensor.ac_213c7f2_48")
+    assert not state
+    state = hass.states.get("binary_sensor.ac_118cdea_2")
+    assert not state
+    state = hass.states.get("binary_sensor.ac_1118cdea_2")
+    assert state
+
+
+async def test_options_add_and_configure_device(hass):
+    """Test we can add a device."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            "host": None,
+            "port": None,
+            "device": "/dev/tty123",
+            "automatic_add": False,
+            "devices": {},
+        },
+        unique_id=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": True,
+            "event_code": "0913000022670e013970",
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "fire_event": False,
+            "signal_repetitions": 5,
+            "data_bits": 4,
+            "off_delay": "abcdef",
+            "command_on": "xyz",
+            "command_off": "xyz",
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+    assert result["errors"]
+    assert result["errors"]["off_delay"] == "invalid_input_off_delay"
+    assert result["errors"]["command_on"] == "invalid_input_2262_on"
+    assert result["errors"]["command_off"] == "invalid_input_2262_off"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "fire_event": False,
+            "signal_repetitions": 5,
+            "data_bits": 4,
+            "command_on": "0xE",
+            "command_off": "0x7",
+            "off_delay": "9",
+        },
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    assert entry.data["automatic_add"]
+
+    assert entry.data["devices"]["0913000022670e013970"]
+    assert not entry.data["devices"]["0913000022670e013970"]["fire_event"]
+    assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5
+    assert entry.data["devices"]["0913000022670e013970"]["off_delay"] == 9
+
+    state = hass.states.get("binary_sensor.pt2262_22670e")
+    assert state
+    assert state.state == "off"
+    assert state.attributes.get("friendly_name") == "PT2262 22670e"
+
+    device_registry = await async_get_device_registry(hass)
+    device_entries = async_entries_for_config_entry(device_registry, entry.entry_id)
+
+    assert device_entries[0].id
+
+    result = await hass.config_entries.options.async_init(entry.entry_id)
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "prompt_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "automatic_add": False,
+            "device": device_entries[0].id,
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "set_device_options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            "fire_event": True,
+            "signal_repetitions": 5,
+            "data_bits": 4,
+            "command_on": "0xE",
+            "command_off": "0x7",
+        },
+    )
+
+    assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    await hass.async_block_till_done()
+
+    assert entry.data["devices"]["0913000022670e013970"]
+    assert entry.data["devices"]["0913000022670e013970"]["fire_event"]
+    assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5
+    assert "delay_off" not in entry.data["devices"]["0913000022670e013970"]
+
+
+def test_get_serial_by_id_no_dir():
+    """Test serial by id conversion if there's no /dev/serial/by-id."""
+    p1 = patch("os.path.isdir", MagicMock(return_value=False))
+    p2 = patch("os.scandir")
+    with p1 as is_dir_mock, p2 as scan_mock:
+        res = config_flow.get_serial_by_id(sentinel.path)
+        assert res is sentinel.path
+        assert is_dir_mock.call_count == 1
+        assert scan_mock.call_count == 0
+
+
+def test_get_serial_by_id():
+    """Test serial by id conversion."""
+    p1 = patch("os.path.isdir", MagicMock(return_value=True))
+    p2 = patch("os.scandir")
+
+    def _realpath(path):
+        if path is sentinel.matched_link:
+            return sentinel.path
+        return sentinel.serial_link_path
+
+    p3 = patch("os.path.realpath", side_effect=_realpath)
+    with p1 as is_dir_mock, p2 as scan_mock, p3:
+        res = config_flow.get_serial_by_id(sentinel.path)
+        assert res is sentinel.path
+        assert is_dir_mock.call_count == 1
+        assert scan_mock.call_count == 1
+
+        entry1 = MagicMock(spec_set=os.DirEntry)
+        entry1.is_symlink.return_value = True
+        entry1.path = sentinel.some_path
+
+        entry2 = MagicMock(spec_set=os.DirEntry)
+        entry2.is_symlink.return_value = False
+        entry2.path = sentinel.other_path
+
+        entry3 = MagicMock(spec_set=os.DirEntry)
+        entry3.is_symlink.return_value = True
+        entry3.path = sentinel.matched_link
+
+        scan_mock.return_value = [entry1, entry2, entry3]
+        res = config_flow.get_serial_by_id(sentinel.path)
+        assert res is sentinel.matched_link
+        assert is_dir_mock.call_count == 2
+        assert scan_mock.call_count == 2
diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py
index 05ce26ebc10..b3e5ce224c6 100644
--- a/tests/components/rfxtrx/test_cover.py
+++ b/tests/components/rfxtrx/test_cover.py
@@ -3,19 +3,23 @@ from unittest.mock import call
 
 import pytest
 
+from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.core import State
-from homeassistant.setup import async_setup_component
 
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
 
 
 async def test_one_cover(hass, rfxtrx):
     """Test with 1 cover."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}},
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("cover.lightwaverf_siemens_0213c7_242")
@@ -57,11 +61,14 @@ async def test_state_restore(hass, rfxtrx, state):
 
     mock_restore_cache(hass, [State(entity_id, state)])
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}},
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     assert hass.states.get(entity_id).state == state
@@ -69,20 +76,18 @@ async def test_state_restore(hass, rfxtrx, state):
 
 async def test_several_covers(hass, rfxtrx):
     """Test with 3 covers."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1400cd0213c7f20d010f51": {},
-                    "0A1400ADF394AB010D0060": {},
-                    "09190000009ba8010100": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1},
+            "0A1400ADF394AB010D0060": {"signal_repetitions": 1},
+            "09190000009ba8010100": {"signal_repetitions": 1},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("cover.lightwaverf_siemens_0213c7_242")
@@ -118,19 +123,17 @@ async def test_discover_covers(hass, rfxtrx_automatic):
 
 async def test_duplicate_cover(hass, rfxtrx):
     """Test with 2 duplicate covers."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1400cd0213c7f20d010f51": {},
-                    "0b1400cd0213c7f20d010f50": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1},
+            "0b1400cd0213c7f20d010f50": {"signal_repetitions": 1},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("cover.lightwaverf_siemens_0213c7_242")
diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py
index abe7c3c0441..037b08b7cc6 100644
--- a/tests/components/rfxtrx/test_init.py
+++ b/tests/components/rfxtrx/test_init.py
@@ -1,10 +1,13 @@
 """The tests for the Rfxtrx component."""
 
+from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT
 from homeassistant.core import callback
 from homeassistant.setup import async_setup_component
 
 from tests.async_mock import call
+from tests.common import MockConfigEntry
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
 
 
 async def test_valid_config(hass):
@@ -55,21 +58,19 @@ async def test_invalid_config(hass):
 
 async def test_fire_event(hass, rfxtrx):
     """Test fire event."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "/dev/serial/by-id/usb"
-                + "-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0",
-                "automatic_add": True,
-                "devices": {
-                    "0b1100cd0213c7f210010f51": {"fire_event": True},
-                    "0716000100900970": {"fire_event": True},
-                },
-            }
+    entry_data = create_rfx_test_cfg(
+        device="/dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0",
+        automatic_add=True,
+        devices={
+            "0b1100cd0213c7f210010f51": {"fire_event": True},
+            "0716000100900970": {"fire_event": True},
         },
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -101,16 +102,19 @@ async def test_fire_event(hass, rfxtrx):
             "type_string": "Byron SX",
             "id_string": "00:90",
             "data": "0716000100900970",
-            "values": {"Sound": 9, "Battery numeric": 0, "Rssi numeric": 7},
+            "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9},
         },
     ]
 
 
 async def test_send(hass, rfxtrx):
     """Test configuration."""
-    assert await async_setup_component(
-        hass, "rfxtrx", {"rfxtrx": {"device": "/dev/null"}}
-    )
+    entry_data = create_rfx_test_cfg(device="/dev/null", devices={})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     await hass.services.async_call(
diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py
index f6f056fa16a..78151c5fa9c 100644
--- a/tests/components/rfxtrx/test_light.py
+++ b/tests/components/rfxtrx/test_light.py
@@ -4,19 +4,23 @@ from unittest.mock import call
 import pytest
 
 from homeassistant.components.light import ATTR_BRIGHTNESS
+from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.core import State
-from homeassistant.setup import async_setup_component
 
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
 
 
 async def test_one_light(hass, rfxtrx):
     """Test with 1 light."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}},
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("light.ac_213c7f2_16")
@@ -95,11 +99,14 @@ async def test_state_restore(hass, rfxtrx, state, brightness):
         hass, [State(entity_id, state, attributes={ATTR_BRIGHTNESS: brightness})]
     )
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}},
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     assert hass.states.get(entity_id).state == state
@@ -108,20 +115,18 @@ async def test_state_restore(hass, rfxtrx, state, brightness):
 
 async def test_several_lights(hass, rfxtrx):
     """Test with 3 lights."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1100cd0213c7f230020f71": {},
-                    "0b1100100118cdea02020f70": {},
-                    "0b1100101118cdea02050f70": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0b1100cd0213c7f230020f71": {"signal_repetitions": 1},
+            "0b1100100118cdea02020f70": {"signal_repetitions": 1},
+            "0b1100101118cdea02050f70": {"signal_repetitions": 1},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -160,18 +165,14 @@ async def test_several_lights(hass, rfxtrx):
 @pytest.mark.parametrize("repetitions", [1, 3])
 async def test_repetitions(hass, rfxtrx, repetitions):
     """Test signal repetitions."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1100cd0213c7f230020f71": {"signal_repetitions": repetitions}
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100cd0213c7f230020f71": {"signal_repetitions": repetitions}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     await hass.services.async_call(
diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py
index d0100e4ea14..c1bb3222b79 100644
--- a/tests/components/rfxtrx/test_sensor.py
+++ b/tests/components/rfxtrx/test_sensor.py
@@ -1,19 +1,23 @@
 """The tests for the Rfxtrx sensor platform."""
 import pytest
 
+from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.components.rfxtrx.const import ATTR_EVENT
 from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS
 from homeassistant.core import State
-from homeassistant.setup import async_setup_component
 
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
 
 
 async def test_default_config(hass, rfxtrx):
     """Test with 0 sensor."""
-    await async_setup_component(
-        hass, "sensor", {"sensor": {"platform": "rfxtrx", "devices": {}}}
-    )
+    entry_data = create_rfx_test_cfg(devices={})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_all()) == 0
@@ -21,11 +25,12 @@ async def test_default_config(hass, rfxtrx):
 
 async def test_one_sensor(hass, rfxtrx):
     """Test with 1 sensor."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0a52080705020095220269": {}}}},
-    )
+    entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_05_02_temperature")
@@ -49,11 +54,12 @@ async def test_state_restore(hass, rfxtrx, state, event):
 
     mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})])
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0a520801070100b81b0279": {}}}},
-    )
+    entry_data = create_rfx_test_cfg(devices={"0a520801070100b81b0279": {}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     assert hass.states.get(entity_id).state == state
@@ -61,11 +67,12 @@ async def test_state_restore(hass, rfxtrx, state, event):
 
 async def test_one_sensor_no_datatype(hass, rfxtrx):
     """Test with 1 sensor."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0a52080705020095220269": {}}}},
-    )
+    entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     base_id = "sensor.wt260_wt260h_wt440h_wt450_wt450h_05_02"
@@ -104,19 +111,17 @@ async def test_one_sensor_no_datatype(hass, rfxtrx):
 
 async def test_several_sensors(hass, rfxtrx):
     """Test with 3 sensors."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0a52080705020095220269": {},
-                    "0a520802060100ff0e0269": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0a52080705020095220269": {},
+            "0a520802060100ff0e0269": {},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -244,19 +249,17 @@ async def test_discover_sensor(hass, rfxtrx_automatic):
 
 async def test_update_of_sensors(hass, rfxtrx):
     """Test with 3 sensors."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0a52080705020095220269": {},
-                    "0a520802060100ff0e0269": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0a52080705020095220269": {},
+            "0a520802060100ff0e0269": {},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
@@ -290,23 +293,21 @@ async def test_update_of_sensors(hass, rfxtrx):
 
 async def test_rssi_sensor(hass, rfxtrx):
     """Test with 1 sensor."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0913000022670e013b70": {
-                        "data_bits": 4,
-                        "command_on": 0xE,
-                        "command_off": 0x7,
-                    },
-                    "0b1100cd0213c7f230010f71": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0913000022670e013b70": {
+                "data_bits": 4,
+                "command_on": 0xE,
+                "command_off": 0x7,
+            },
+            "0b1100cd0213c7f230010f71": {},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
     await hass.async_start()
 
diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py
index 1fed6a65562..ee4fd265fc9 100644
--- a/tests/components/rfxtrx/test_switch.py
+++ b/tests/components/rfxtrx/test_switch.py
@@ -5,9 +5,9 @@ import pytest
 
 from homeassistant.components.rfxtrx import DOMAIN
 from homeassistant.core import State
-from homeassistant.setup import async_setup_component
 
-from tests.common import mock_restore_cache
+from tests.common import MockConfigEntry, mock_restore_cache
+from tests.components.rfxtrx.conftest import create_rfx_test_cfg
 
 EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113"
 EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114"
@@ -15,11 +15,14 @@ EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114"
 
 async def test_one_switch(hass, rfxtrx):
     """Test with 1 switch."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}},
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("switch.ac_213c7f2_16")
@@ -55,11 +58,14 @@ async def test_state_restore(hass, rfxtrx, state):
 
     mock_restore_cache(hass, [State(entity_id, state)])
 
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}},
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     assert hass.states.get(entity_id).state == state
@@ -67,20 +73,18 @@ async def test_state_restore(hass, rfxtrx, state):
 
 async def test_several_switches(hass, rfxtrx):
     """Test with 3 switches."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1100cd0213c7f230010f71": {},
-                    "0b1100100118cdea02010f70": {},
-                    "0b1100101118cdea02010f70": {},
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={
+            "0b1100cd0213c7f230010f71": {"signal_repetitions": 1},
+            "0b1100100118cdea02010f70": {"signal_repetitions": 1},
+            "0b1100101118cdea02010f70": {"signal_repetitions": 1},
+        }
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     state = hass.states.get("switch.ac_213c7f2_48")
@@ -102,18 +106,14 @@ async def test_several_switches(hass, rfxtrx):
 @pytest.mark.parametrize("repetitions", [1, 3])
 async def test_repetitions(hass, rfxtrx, repetitions):
     """Test signal repetitions."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {
-                    "0b1100cd0213c7f230010f71": {"signal_repetitions": repetitions}
-                },
-            }
-        },
+    entry_data = create_rfx_test_cfg(
+        devices={"0b1100cd0213c7f230010f71": {"signal_repetitions": repetitions}}
     )
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     await hass.services.async_call(
@@ -156,16 +156,12 @@ async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic):
 
 async def test_unknown_event_code(hass, rfxtrx):
     """Test with 3 switches."""
-    assert await async_setup_component(
-        hass,
-        "rfxtrx",
-        {
-            "rfxtrx": {
-                "device": "abcd",
-                "devices": {"1234567890": {}},
-            }
-        },
-    )
+    entry_data = create_rfx_test_cfg(devices={"1234567890": {"signal_repetitions": 1}})
+    mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data)
+
+    mock_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_entry.entry_id)
     await hass.async_block_till_done()
 
     conf_entries = hass.config_entries.async_entries(DOMAIN)
-- 
GitLab