From e1ae940a34912836966b8a2f4361577c9640b709 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
Date: Wed, 23 Mar 2022 00:01:24 -0400
Subject: [PATCH] Add config flow to deluge (#58789)

---
 .coveragerc                                   |   2 +
 CODEOWNERS                                    |   2 +
 homeassistant/components/deluge/__init__.py   |  88 +++++++++-
 .../components/deluge/config_flow.py          | 125 ++++++++++++++
 homeassistant/components/deluge/const.py      |  12 ++
 .../components/deluge/coordinator.py          |  68 ++++++++
 homeassistant/components/deluge/manifest.json |   3 +-
 homeassistant/components/deluge/sensor.py     | 153 +++++++----------
 homeassistant/components/deluge/strings.json  |  23 +++
 homeassistant/components/deluge/switch.py     | 120 +++++--------
 .../components/deluge/translations/en.json    |  23 +++
 homeassistant/generated/config_flows.py       |   1 +
 requirements_test_all.txt                     |   3 +
 tests/components/deluge/__init__.py           |  32 ++++
 tests/components/deluge/test_config_flow.py   | 160 ++++++++++++++++++
 15 files changed, 651 insertions(+), 164 deletions(-)
 create mode 100644 homeassistant/components/deluge/config_flow.py
 create mode 100644 homeassistant/components/deluge/const.py
 create mode 100644 homeassistant/components/deluge/coordinator.py
 create mode 100644 homeassistant/components/deluge/strings.json
 create mode 100644 homeassistant/components/deluge/translations/en.json
 create mode 100644 tests/components/deluge/__init__.py
 create mode 100644 tests/components/deluge/test_config_flow.py

diff --git a/.coveragerc b/.coveragerc
index 1216a78370a..06d5265c9f2 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -197,6 +197,8 @@ omit =
     homeassistant/components/decora/light.py
     homeassistant/components/decora_wifi/light.py
     homeassistant/components/delijn/*
+    homeassistant/components/deluge/__init__.py
+    homeassistant/components/deluge/coordinator.py
     homeassistant/components/deluge/sensor.py
     homeassistant/components/deluge/switch.py
     homeassistant/components/denon/media_player.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 6ce57254f5a..bc801a5f8f0 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -210,6 +210,8 @@ tests/components/deconz/* @Kane610
 homeassistant/components/default_config/* @home-assistant/core
 tests/components/default_config/* @home-assistant/core
 homeassistant/components/delijn/* @bollewolle @Emilv2
+homeassistant/components/deluge/* @tkdrob
+tests/components/deluge/* @tkdrob
 homeassistant/components/demo/* @home-assistant/core
 tests/components/demo/* @home-assistant/core
 homeassistant/components/denonavr/* @ol-iver @starkillerOG
diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py
index ad40b688fcf..d8d6945be2a 100644
--- a/homeassistant/components/deluge/__init__.py
+++ b/homeassistant/components/deluge/__init__.py
@@ -1 +1,87 @@
-"""The deluge component."""
+"""The Deluge integration."""
+from __future__ import annotations
+
+import logging
+import socket
+from ssl import SSLError
+
+from deluge_client.client import DelugeRPCClient
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_USERNAME,
+    Platform,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
+from homeassistant.helpers.device_registry import DeviceEntryType
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import CONF_WEB_PORT, DEFAULT_NAME, DOMAIN
+from .coordinator import DelugeDataUpdateCoordinator
+
+PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Deluge from a config entry."""
+    host = entry.data[CONF_HOST]
+    port = entry.data[CONF_PORT]
+    username = entry.data[CONF_USERNAME]
+    password = entry.data[CONF_PASSWORD]
+    api = await hass.async_add_executor_job(
+        DelugeRPCClient, host, port, username, password
+    )
+    api.web_port = entry.data[CONF_WEB_PORT]
+    try:
+        await hass.async_add_executor_job(api.connect)
+    except (
+        ConnectionRefusedError,
+        socket.timeout,  # pylint:disable=no-member
+        SSLError,
+    ) as ex:
+        raise ConfigEntryNotReady("Connection to Deluge Daemon failed") from ex
+    except Exception as ex:  # pylint:disable=broad-except
+        if type(ex).__name__ == "BadLoginError":
+            raise ConfigEntryAuthFailed(
+                "Credentials for Deluge client are not valid"
+            ) from ex
+        _LOGGER.error("Unknown error connecting to Deluge: %s", ex)
+
+    coordinator = DelugeDataUpdateCoordinator(hass, api, entry)
+    await coordinator.async_config_entry_first_refresh()
+    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
+    hass.config_entries.async_setup_platforms(entry, PLATFORMS)
+
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload a config entry."""
+    if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+        hass.data[DOMAIN].pop(entry.entry_id)
+
+    return unload_ok
+
+
+class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]):
+    """Representation of a Deluge entity."""
+
+    def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None:
+        """Initialize a Deluge entity."""
+        super().__init__(coordinator)
+        self._server_unique_id = coordinator.config_entry.entry_id
+        self._attr_device_info = DeviceInfo(
+            configuration_url=f"http://{coordinator.api.host}:{coordinator.api.web_port}",
+            entry_type=DeviceEntryType.SERVICE,
+            identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
+            manufacturer=DEFAULT_NAME,
+            name=DEFAULT_NAME,
+            sw_version=coordinator.api.deluge_version,
+        )
diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py
new file mode 100644
index 00000000000..a15c0608029
--- /dev/null
+++ b/homeassistant/components/deluge/config_flow.py
@@ -0,0 +1,125 @@
+"""Config flow for the Deluge integration."""
+from __future__ import annotations
+
+import logging
+import socket
+from ssl import SSLError
+from typing import Any
+
+from deluge_client.client import DelugeRPCClient
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_MONITORED_VARIABLES,
+    CONF_NAME,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_SOURCE,
+    CONF_USERNAME,
+)
+from homeassistant.data_entry_flow import FlowResult
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+    CONF_WEB_PORT,
+    DEFAULT_NAME,
+    DEFAULT_RPC_PORT,
+    DEFAULT_WEB_PORT,
+    DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for Deluge."""
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle a flow initiated by the user."""
+        errors = {}
+        title = None
+
+        if user_input is not None:
+            if CONF_NAME in user_input:
+                title = user_input.pop(CONF_NAME)
+            if (error := await self.validate_input(user_input)) is None:
+                for entry in self._async_current_entries():
+                    if (
+                        user_input[CONF_HOST] == entry.data[CONF_HOST]
+                        and user_input[CONF_PORT] == entry.data[CONF_PORT]
+                    ):
+                        if self.context.get(CONF_SOURCE) == SOURCE_REAUTH:
+                            self.hass.config_entries.async_update_entry(
+                                entry, data=user_input
+                            )
+                            await self.hass.config_entries.async_reload(entry.entry_id)
+                            return self.async_abort(reason="reauth_successful")
+                        return self.async_abort(reason="already_configured")
+                return self.async_create_entry(
+                    title=title or DEFAULT_NAME,
+                    data=user_input,
+                )
+            errors["base"] = error
+        user_input = user_input or {}
+        schema = vol.Schema(
+            {
+                vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): cv.string,
+                vol.Required(
+                    CONF_USERNAME, default=user_input.get(CONF_USERNAME)
+                ): cv.string,
+                vol.Required(CONF_PASSWORD, default=""): cv.string,
+                vol.Optional(
+                    CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_RPC_PORT)
+                ): int,
+                vol.Optional(
+                    CONF_WEB_PORT,
+                    default=user_input.get(CONF_WEB_PORT, DEFAULT_WEB_PORT),
+                ): int,
+            }
+        )
+        return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
+
+    async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult:
+        """Handle a reauthorization flow request."""
+        return await self.async_step_user()
+
+    async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
+        """Import a config entry from configuration.yaml."""
+        if CONF_MONITORED_VARIABLES in config:
+            config.pop(CONF_MONITORED_VARIABLES)
+        config[CONF_WEB_PORT] = DEFAULT_WEB_PORT
+
+        for entry in self._async_current_entries():
+            if entry.data[CONF_HOST] == config[CONF_HOST]:
+                _LOGGER.warning(
+                    "Deluge yaml config has been imported. Please remove it"
+                )
+                return self.async_abort(reason="already_configured")
+        return await self.async_step_user(config)
+
+    async def validate_input(self, user_input: dict[str, Any]) -> str | None:
+        """Handle common flow input validation."""
+        host = user_input[CONF_HOST]
+        port = user_input[CONF_PORT]
+        username = user_input[CONF_USERNAME]
+        password = user_input[CONF_PASSWORD]
+        api = DelugeRPCClient(
+            host=host, port=port, username=username, password=password
+        )
+        try:
+            await self.hass.async_add_executor_job(api.connect)
+        except (
+            ConnectionRefusedError,
+            socket.timeout,  # pylint:disable=no-member
+            SSLError,
+        ):
+            return "cannot_connect"
+        except Exception as ex:  # pylint:disable=broad-except
+            if type(ex).__name__ == "BadLoginError":
+                return "invalid_auth"  # pragma: no cover
+            return "unknown"
+        return None
diff --git a/homeassistant/components/deluge/const.py b/homeassistant/components/deluge/const.py
new file mode 100644
index 00000000000..505c20e860f
--- /dev/null
+++ b/homeassistant/components/deluge/const.py
@@ -0,0 +1,12 @@
+"""Constants for the Deluge integration."""
+import logging
+
+CONF_WEB_PORT = "web_port"
+DEFAULT_NAME = "Deluge"
+DEFAULT_RPC_PORT = 58846
+DEFAULT_WEB_PORT = 8112
+DHT_UPLOAD = 1000
+DHT_DOWNLOAD = 1000
+DOMAIN = "deluge"
+
+LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py
new file mode 100644
index 00000000000..36c2d5d2240
--- /dev/null
+++ b/homeassistant/components/deluge/coordinator.py
@@ -0,0 +1,68 @@
+"""Data update coordinator for the Deluge integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import socket
+from ssl import SSLError
+
+from deluge_client.client import DelugeRPCClient, FailedToReconnectException
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryAuthFailed
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import LOGGER
+
+
+class DelugeDataUpdateCoordinator(DataUpdateCoordinator):
+    """Data update coordinator for the Deluge integration."""
+
+    config_entry: ConfigEntry
+
+    def __init__(
+        self, hass: HomeAssistant, api: DelugeRPCClient, entry: ConfigEntry
+    ) -> None:
+        """Initialize the coordinator."""
+        super().__init__(
+            hass=hass,
+            logger=LOGGER,
+            name=entry.title,
+            update_interval=timedelta(seconds=30),
+        )
+        self.api = api
+        self.config_entry = entry
+
+    async def _async_update_data(self) -> dict[Platform, dict[str, int | str]]:
+        """Get the latest data from Deluge and updates the state."""
+        data = {}
+        try:
+            data[Platform.SENSOR] = await self.hass.async_add_executor_job(
+                self.api.call,
+                "core.get_session_status",
+                [
+                    "upload_rate",
+                    "download_rate",
+                    "dht_upload_rate",
+                    "dht_download_rate",
+                ],
+            )
+            data[Platform.SWITCH] = await self.hass.async_add_executor_job(
+                self.api.call, "core.get_torrents_status", {}, ["paused"]
+            )
+        except (
+            ConnectionRefusedError,
+            socket.timeout,  # pylint:disable=no-member
+            SSLError,
+            FailedToReconnectException,
+        ) as ex:
+            raise UpdateFailed(f"Connection to Deluge Daemon Lost: {ex}") from ex
+        except Exception as ex:  # pylint:disable=broad-except
+            if type(ex).__name__ == "BadLoginError":
+                raise ConfigEntryAuthFailed(
+                    "Credentials for Deluge client are not valid"
+                ) from ex
+            LOGGER.error("Unknown error connecting to Deluge: %s", ex)
+            raise ex
+        return data
diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json
index 5bf4651096c..920e560b70f 100644
--- a/homeassistant/components/deluge/manifest.json
+++ b/homeassistant/components/deluge/manifest.json
@@ -3,7 +3,8 @@
   "name": "Deluge",
   "documentation": "https://www.home-assistant.io/integrations/deluge",
   "requirements": ["deluge-client==1.7.1"],
-  "codeowners": [],
+  "codeowners": ["@tkdrob"],
+  "config_flow": true,
   "iot_class": "local_polling",
   "loggers": ["deluge_client"]
 }
diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py
index 01241b0d120..99e63e6ef17 100644
--- a/homeassistant/components/deluge/sensor.py
+++ b/homeassistant/components/deluge/sensor.py
@@ -1,16 +1,15 @@
 """Support for monitoring the Deluge BitTorrent client API."""
 from __future__ import annotations
 
-import logging
-
-from deluge_client import DelugeRPCClient, FailedToReconnectException
 import voluptuous as vol
 
 from homeassistant.components.sensor import (
     PLATFORM_SCHEMA,
     SensorEntity,
     SensorEntityDescription,
+    SensorStateClass,
 )
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import (
     CONF_HOST,
     CONF_MONITORED_VARIABLES,
@@ -20,20 +19,17 @@ from homeassistant.const import (
     CONF_USERNAME,
     DATA_RATE_KILOBYTES_PER_SECOND,
     STATE_IDLE,
+    Platform,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers import entity_platform
 import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
 
-_LOGGER = logging.getLogger(__name__)
-_THROTTLED_REFRESH = None
+from . import DelugeEntity
+from .const import DEFAULT_NAME, DEFAULT_RPC_PORT, DOMAIN
+from .coordinator import DelugeDataUpdateCoordinator
 
-DEFAULT_NAME = "Deluge"
-DEFAULT_PORT = 58846
-DHT_UPLOAD = 1000
-DHT_DOWNLOAD = 1000
 SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key="current_status",
@@ -43,21 +39,24 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key="download_speed",
         name="Down Speed",
         native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND,
+        state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
         key="upload_speed",
         name="Up Speed",
         native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND,
+        state_class=SensorStateClass.MEASUREMENT,
     ),
 )
 
 SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
 
+# Deprecated in Home Assistant 2022.3
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
         vol.Required(CONF_HOST): cv.string,
         vol.Required(CONF_PASSWORD): cv.string,
-        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+        vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port,
         vol.Required(CONF_USERNAME): cv.string,
         vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
         vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All(
@@ -67,92 +66,70 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
 )
 
 
-def setup_platform(
+async def async_setup_platform(
     hass: HomeAssistant,
     config: ConfigType,
-    add_entities: AddEntitiesCallback,
+    async_add_entities: entity_platform.AddEntitiesCallback,
     discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
-    """Set up the Deluge sensors."""
+    """Set up the Deluge sensor component."""
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": SOURCE_IMPORT}, data=config
+        )
+    )
 
-    name = config[CONF_NAME]
-    host = config[CONF_HOST]
-    username = config[CONF_USERNAME]
-    password = config[CONF_PASSWORD]
-    port = config[CONF_PORT]
 
-    deluge_api = DelugeRPCClient(host, port, username, password)
-    try:
-        deluge_api.connect()
-    except ConnectionRefusedError as err:
-        _LOGGER.error("Connection to Deluge Daemon failed")
-        raise PlatformNotReady from err
-    monitored_variables = config[CONF_MONITORED_VARIABLES]
-    entities = [
-        DelugeSensor(deluge_api, name, description)
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: entity_platform.AddEntitiesCallback,
+) -> None:
+    """Set up the Deluge sensor."""
+    async_add_entities(
+        DelugeSensor(hass.data[DOMAIN][entry.entry_id], description)
         for description in SENSOR_TYPES
-        if description.key in monitored_variables
-    ]
+    )
 
-    add_entities(entities)
 
-
-class DelugeSensor(SensorEntity):
+class DelugeSensor(DelugeEntity, SensorEntity):
     """Representation of a Deluge sensor."""
 
     def __init__(
-        self, deluge_client, client_name, description: SensorEntityDescription
-    ):
+        self,
+        coordinator: DelugeDataUpdateCoordinator,
+        description: SensorEntityDescription,
+    ) -> None:
         """Initialize the sensor."""
+        super().__init__(coordinator)
         self.entity_description = description
-        self.client = deluge_client
-        self.data = None
-
-        self._attr_available = False
-        self._attr_name = f"{client_name} {description.name}"
-
-    def update(self):
-        """Get the latest data from Deluge and updates the state."""
-
-        try:
-            self.data = self.client.call(
-                "core.get_session_status",
-                [
-                    "upload_rate",
-                    "download_rate",
-                    "dht_upload_rate",
-                    "dht_download_rate",
-                ],
-            )
-            self._attr_available = True
-        except FailedToReconnectException:
-            _LOGGER.error("Connection to Deluge Daemon Lost")
-            self._attr_available = False
-            return
-
-        upload = self.data[b"upload_rate"] - self.data[b"dht_upload_rate"]
-        download = self.data[b"download_rate"] - self.data[b"dht_download_rate"]
-
-        sensor_type = self.entity_description.key
-        if sensor_type == "current_status":
-            if self.data:
-                if upload > 0 and download > 0:
-                    self._attr_native_value = "Up/Down"
-                elif upload > 0 and download == 0:
-                    self._attr_native_value = "Seeding"
-                elif upload == 0 and download > 0:
-                    self._attr_native_value = "Downloading"
-                else:
-                    self._attr_native_value = STATE_IDLE
-            else:
-                self._attr_native_value = None
-
-        if self.data:
-            if sensor_type == "download_speed":
-                kb_spd = float(download)
-                kb_spd = kb_spd / 1024
-                self._attr_native_value = round(kb_spd, 2 if kb_spd < 0.1 else 1)
-            elif sensor_type == "upload_speed":
-                kb_spd = float(upload)
-                kb_spd = kb_spd / 1024
-                self._attr_native_value = round(kb_spd, 2 if kb_spd < 0.1 else 1)
+        self._attr_name = f"{coordinator.config_entry.title} {description.name}"
+        self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
+
+    @property
+    def native_value(self) -> StateType:
+        """Return the state of the sensor."""
+        if self.coordinator.data:
+            data = self.coordinator.data[Platform.SENSOR]
+            upload = data[b"upload_rate"] - data[b"dht_upload_rate"]
+            download = data[b"download_rate"] - data[b"dht_download_rate"]
+            if self.entity_description.key == "current_status":
+                if data:
+                    if upload > 0 and download > 0:
+                        return "Up/Down"
+                    if upload > 0 and download == 0:
+                        return "Seeding"
+                    if upload == 0 and download > 0:
+                        return "Downloading"
+                    return STATE_IDLE
+
+            if data:
+                if self.entity_description.key == "download_speed":
+                    kb_spd = float(download)
+                    kb_spd = kb_spd / 1024
+                    return round(kb_spd, 2 if kb_spd < 0.1 else 1)
+                if self.entity_description.key == "upload_speed":
+                    kb_spd = float(upload)
+                    kb_spd = kb_spd / 1024
+                    return round(kb_spd, 2 if kb_spd < 0.1 else 1)
+        return None
diff --git a/homeassistant/components/deluge/strings.json b/homeassistant/components/deluge/strings.json
new file mode 100644
index 00000000000..9474e25a534
--- /dev/null
+++ b/homeassistant/components/deluge/strings.json
@@ -0,0 +1,23 @@
+{
+  "config": {
+    "step": {
+      "user": {
+        "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls",
+        "data": {
+          "host": "[%key:common::config_flow::data::host%]",
+          "username": "[%key:common::config_flow::data::username%]",
+          "password": "[%key:common::config_flow::data::password%]",
+          "port": "[%key:common::config_flow::data::port%]",
+          "web_port": "Web port (for visiting service)"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
+    }
+  }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py
index 16a5f023052..d438d236e5c 100644
--- a/homeassistant/components/deluge/switch.py
+++ b/homeassistant/components/deluge/switch.py
@@ -1,118 +1,90 @@
 """Support for setting the Deluge BitTorrent client in Pause."""
 from __future__ import annotations
 
-import logging
+from typing import Any
 
-from deluge_client import DelugeRPCClient, FailedToReconnectException
 import voluptuous as vol
 
 from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import (
     CONF_HOST,
     CONF_NAME,
     CONF_PASSWORD,
     CONF_PORT,
     CONF_USERNAME,
-    STATE_OFF,
-    STATE_ON,
+    Platform,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers import entity_platform
 import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
-_LOGGER = logging.getLogger(__name__)
-
-DEFAULT_NAME = "Deluge Switch"
-DEFAULT_PORT = 58846
+from . import DelugeEntity
+from .const import DEFAULT_RPC_PORT, DOMAIN
+from .coordinator import DelugeDataUpdateCoordinator
 
+# Deprecated in Home Assistant 2022.3
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
         vol.Required(CONF_HOST): cv.string,
         vol.Required(CONF_PASSWORD): cv.string,
-        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+        vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port,
         vol.Required(CONF_USERNAME): cv.string,
-        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+        vol.Optional(CONF_NAME, default="Deluge Switch"): cv.string,
     }
 )
 
 
-def setup_platform(
+async def async_setup_platform(
     hass: HomeAssistant,
     config: ConfigType,
-    add_entities: AddEntitiesCallback,
+    async_add_entities: entity_platform.AddEntitiesCallback,
     discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
-    """Set up the Deluge switch."""
+    """Set up the Deluge sensor component."""
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": SOURCE_IMPORT}, data=config
+        )
+    )
 
-    name = config[CONF_NAME]
-    host = config[CONF_HOST]
-    username = config[CONF_USERNAME]
-    password = config[CONF_PASSWORD]
-    port = config[CONF_PORT]
 
-    deluge_api = DelugeRPCClient(host, port, username, password)
-    try:
-        deluge_api.connect()
-    except ConnectionRefusedError as err:
-        _LOGGER.error("Connection to Deluge Daemon failed")
-        raise PlatformNotReady from err
-
-    add_entities([DelugeSwitch(deluge_api, name)])
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: entity_platform.AddEntitiesCallback,
+) -> None:
+    """Set up the Deluge switch."""
+    async_add_entities([DelugeSwitch(hass.data[DOMAIN][entry.entry_id])])
 
 
-class DelugeSwitch(SwitchEntity):
+class DelugeSwitch(DelugeEntity, SwitchEntity):
     """Representation of a Deluge switch."""
 
-    def __init__(self, deluge_client, name):
+    def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None:
         """Initialize the Deluge switch."""
-        self._name = name
-        self.deluge_client = deluge_client
-        self._state = STATE_OFF
-        self._available = False
-
-    @property
-    def name(self):
-        """Return the name of the switch."""
-        return self._name
+        super().__init__(coordinator)
+        self._attr_name = coordinator.config_entry.title
+        self._attr_unique_id = f"{coordinator.config_entry.entry_id}_enabled"
 
-    @property
-    def is_on(self):
-        """Return true if device is on."""
-        return self._state == STATE_ON
-
-    @property
-    def available(self):
-        """Return true if device is available."""
-        return self._available
-
-    def turn_on(self, **kwargs):
+    def turn_on(self, **kwargs: Any) -> None:
         """Turn the device on."""
-        torrent_ids = self.deluge_client.call("core.get_session_state")
-        self.deluge_client.call("core.resume_torrent", torrent_ids)
+        torrent_ids = self.coordinator.api.call("core.get_session_state")
+        self.coordinator.api.call("core.resume_torrent", torrent_ids)
 
-    def turn_off(self, **kwargs):
+    def turn_off(self, **kwargs: Any) -> None:
         """Turn the device off."""
-        torrent_ids = self.deluge_client.call("core.get_session_state")
-        self.deluge_client.call("core.pause_torrent", torrent_ids)
+        torrent_ids = self.coordinator.api.call("core.get_session_state")
+        self.coordinator.api.call("core.pause_torrent", torrent_ids)
 
-    def update(self):
-        """Get the latest data from deluge and updates the state."""
-
-        try:
-            torrent_list = self.deluge_client.call(
-                "core.get_torrents_status", {}, ["paused"]
-            )
-            self._available = True
-        except FailedToReconnectException:
-            _LOGGER.error("Connection to Deluge Daemon Lost")
-            self._available = False
-            return
-        for torrent in torrent_list.values():
-            item = torrent.popitem()
-            if not item[1]:
-                self._state = STATE_ON
-                return
-
-        self._state = STATE_OFF
+    @property
+    def is_on(self) -> bool:
+        """Return state of the switch."""
+        if self.coordinator.data:
+            data: dict = self.coordinator.data[Platform.SWITCH]
+            for torrent in data.values():
+                item = torrent.popitem()
+                if not item[1]:
+                    return True
+        return False
diff --git a/homeassistant/components/deluge/translations/en.json b/homeassistant/components/deluge/translations/en.json
new file mode 100644
index 00000000000..a3a2f539126
--- /dev/null
+++ b/homeassistant/components/deluge/translations/en.json
@@ -0,0 +1,23 @@
+{
+    "config": {
+        "step": {
+            "user": {
+                "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls",
+                "data": {
+                    "host": "Host",
+                    "username": "Username",
+                    "password": "Password",
+                    "port": "Port",
+                    "web_port": "Web port (for visiting service)"
+                }
+            }
+        },
+        "error": {
+            "cannot_connect": "Failed to connect",
+            "invalid_auth": "Invalid authentication"
+        },
+        "abort": {
+            "already_configured": "Service is already configured"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 25d6ec2c807..e1b6e95800d 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -66,6 +66,7 @@ FLOWS = {
         "crownstone",
         "daikin",
         "deconz",
+        "deluge",
         "denonavr",
         "devolo_home_control",
         "devolo_home_network",
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 3bfd6769e08..357e9a10a9e 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -373,6 +373,9 @@ debugpy==1.5.1
 # homeassistant.components.ohmconnect
 defusedxml==0.7.1
 
+# homeassistant.components.deluge
+deluge-client==1.7.1
+
 # homeassistant.components.denonavr
 denonavr==0.10.10
 
diff --git a/tests/components/deluge/__init__.py b/tests/components/deluge/__init__.py
new file mode 100644
index 00000000000..47339f8dfd5
--- /dev/null
+++ b/tests/components/deluge/__init__.py
@@ -0,0 +1,32 @@
+"""Tests for the Deluge integration."""
+
+from homeassistant.components.deluge.const import (
+    CONF_WEB_PORT,
+    DEFAULT_RPC_PORT,
+    DEFAULT_WEB_PORT,
+)
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_MONITORED_VARIABLES,
+    CONF_NAME,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_USERNAME,
+)
+
+CONF_DATA = {
+    CONF_HOST: "1.2.3.4",
+    CONF_USERNAME: "user",
+    CONF_PASSWORD: "password",
+    CONF_PORT: DEFAULT_RPC_PORT,
+    CONF_WEB_PORT: DEFAULT_WEB_PORT,
+}
+
+IMPORT_DATA = {
+    CONF_HOST: "1.2.3.4",
+    CONF_NAME: "Deluge Torrent",
+    CONF_MONITORED_VARIABLES: ["current_status", "download_speed", "upload_speed"],
+    CONF_USERNAME: "user",
+    CONF_PASSWORD: "password",
+    CONF_PORT: DEFAULT_RPC_PORT,
+}
diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py
new file mode 100644
index 00000000000..56dbac55674
--- /dev/null
+++ b/tests/components/deluge/test_config_flow.py
@@ -0,0 +1,160 @@
+"""Test Deluge config flow."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER
+from homeassistant.const import CONF_SOURCE
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import (
+    RESULT_TYPE_ABORT,
+    RESULT_TYPE_CREATE_ENTRY,
+    RESULT_TYPE_FORM,
+)
+
+from . import CONF_DATA, IMPORT_DATA
+
+from tests.common import MockConfigEntry
+
+
+@pytest.fixture(name="api")
+def mock_deluge_api():
+    """Mock an api."""
+    with patch("deluge_client.client.DelugeRPCClient.connect"), patch(
+        "deluge_client.client.DelugeRPCClient._create_socket"
+    ):
+        yield
+
+
+@pytest.fixture(name="conn_error")
+def mock_api_connection_error():
+    """Mock an api."""
+    with patch(
+        "deluge_client.client.DelugeRPCClient.connect",
+        side_effect=ConnectionRefusedError("111: Connection refused"),
+    ), patch("deluge_client.client.DelugeRPCClient._create_socket"):
+        yield
+
+
+@pytest.fixture(name="unknown_error")
+def mock_api_unknown_error():
+    """Mock an api."""
+    with patch(
+        "deluge_client.client.DelugeRPCClient.connect", side_effect=Exception
+    ), patch("deluge_client.client.DelugeRPCClient._create_socket"):
+        yield
+
+
+@pytest.fixture(name="deluge_setup", autouse=True)
+def deluge_setup_fixture():
+    """Mock deluge entry setup."""
+    with patch("homeassistant.components.deluge.async_setup_entry", return_value=True):
+        yield
+
+
+async def test_flow_user(hass: HomeAssistant, api):
+    """Test user initialized flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+        data=CONF_DATA,
+    )
+    assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+    assert result["title"] == DEFAULT_NAME
+    assert result["data"] == CONF_DATA
+
+
+async def test_flow_user_already_configured(hass: HomeAssistant, api):
+    """Test user initialized flow with duplicate server."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data=CONF_DATA,
+    )
+
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "already_configured"
+
+
+async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error):
+    """Test user initialized flow with unreachable server."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
+    )
+    assert result["type"] == RESULT_TYPE_FORM
+    assert result["step_id"] == "user"
+    assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error):
+    """Test user initialized flow with unreachable server."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA
+    )
+    assert result["type"] == RESULT_TYPE_FORM
+    assert result["step_id"] == "user"
+    assert result["errors"] == {"base": "unknown"}
+
+
+async def test_flow_import(hass: HomeAssistant, api):
+    """Test import step."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA
+    )
+
+    assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+    assert result["title"] == "Deluge Torrent"
+    assert result["data"] == CONF_DATA
+
+
+async def test_flow_import_already_configured(hass: HomeAssistant, api):
+    """Test import step already configured."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data=CONF_DATA,
+    )
+
+    entry.add_to_hass(hass)
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "already_configured"
+
+
+async def test_flow_reauth(hass: HomeAssistant, api):
+    """Test reauth step."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data=CONF_DATA,
+    )
+
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={
+            CONF_SOURCE: SOURCE_REAUTH,
+            "entry_id": entry.entry_id,
+            "unique_id": entry.unique_id,
+        },
+        data=CONF_DATA,
+    )
+
+    assert result["type"] == RESULT_TYPE_FORM
+    assert result["step_id"] == "user"
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input=CONF_DATA,
+    )
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "reauth_successful"
+    assert entry.data == CONF_DATA
-- 
GitLab