From be0926b7b8cb08415832d81e6559a43f062c85a7 Mon Sep 17 00:00:00 2001
From: Sid <27780930+autinerd@users.noreply.github.com>
Date: Wed, 17 Apr 2024 15:21:54 +0200
Subject: [PATCH] Add config flow to enigma2 (#106348)

* add config flow to enigma2

* Apply suggestions from code review

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fix suggested change

* use parametrize for config flow tests

* Restore PLATFORM_SCHEMA and add create_issue to async_setup_platform

* fix docstring

* remove name, refactor config flow

* bump dependency

* remove name, add verify_ssl, use async_create_clientsession

* use translation key, change integration type to device

* Bump openwebifpy to 4.2.1

* cleanup, remove CONF_NAME from entity, add async_set_unique_id

* clear unneeded constants, fix tests

* fix tests

* move _attr_translation_key out of init

* update test requirement

* Address review comments

* address review comments

* clear strings.json

* Review coments

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
---
 CODEOWNERS                                    |   1 +
 homeassistant/components/enigma2/__init__.py  |  47 ++++++
 .../components/enigma2/config_flow.py         | 158 ++++++++++++++++++
 homeassistant/components/enigma2/const.py     |   1 +
 .../components/enigma2/manifest.json          |   2 +
 .../components/enigma2/media_player.py        |  80 ++++-----
 homeassistant/components/enigma2/strings.json |  30 ++++
 homeassistant/generated/config_flows.py       |   1 +
 homeassistant/generated/integrations.json     |   4 +-
 requirements_test_all.txt                     |   3 +
 tests/components/enigma2/__init__.py          |   1 +
 tests/components/enigma2/conftest.py          |  90 ++++++++++
 tests/components/enigma2/test_config_flow.py  | 149 +++++++++++++++++
 tests/components/enigma2/test_init.py         |  38 +++++
 14 files changed, 565 insertions(+), 40 deletions(-)
 create mode 100644 homeassistant/components/enigma2/config_flow.py
 create mode 100644 homeassistant/components/enigma2/strings.json
 create mode 100644 tests/components/enigma2/__init__.py
 create mode 100644 tests/components/enigma2/conftest.py
 create mode 100644 tests/components/enigma2/test_config_flow.py
 create mode 100644 tests/components/enigma2/test_init.py

diff --git a/CODEOWNERS b/CODEOWNERS
index a4224025acc..b2de3031cf8 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -389,6 +389,7 @@ build.json @home-assistant/supervisor
 /homeassistant/components/energyzero/ @klaasnicolaas
 /tests/components/energyzero/ @klaasnicolaas
 /homeassistant/components/enigma2/ @autinerd
+/tests/components/enigma2/ @autinerd
 /homeassistant/components/enocean/ @bdurrer
 /tests/components/enocean/ @bdurrer
 /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
diff --git a/homeassistant/components/enigma2/__init__.py b/homeassistant/components/enigma2/__init__.py
index 11cd4d9a804..241ca7444fb 100644
--- a/homeassistant/components/enigma2/__init__.py
+++ b/homeassistant/components/enigma2/__init__.py
@@ -1 +1,48 @@
 """Support for Enigma2 devices."""
+
+from openwebif.api import OpenWebIfDevice
+from yarl import URL
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_SSL,
+    CONF_USERNAME,
+    CONF_VERIFY_SSL,
+    Platform,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_create_clientsession
+
+from .const import DOMAIN
+
+PLATFORMS = [Platform.MEDIA_PLAYER]
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Enigma2 from a config entry."""
+    base_url = URL.build(
+        scheme="http" if not entry.data[CONF_SSL] else "https",
+        host=entry.data[CONF_HOST],
+        port=entry.data[CONF_PORT],
+        user=entry.data.get(CONF_USERNAME),
+        password=entry.data.get(CONF_PASSWORD),
+    )
+
+    session = async_create_clientsession(
+        hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url
+    )
+
+    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = OpenWebIfDevice(session)
+    await hass.config_entries.async_forward_entry_setups(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
diff --git a/homeassistant/components/enigma2/config_flow.py b/homeassistant/components/enigma2/config_flow.py
new file mode 100644
index 00000000000..c144f2b7dae
--- /dev/null
+++ b/homeassistant/components/enigma2/config_flow.py
@@ -0,0 +1,158 @@
+"""Config flow for Enigma2."""
+
+from typing import Any
+
+from aiohttp.client_exceptions import ClientError
+from openwebif.api import OpenWebIfDevice
+from openwebif.error import InvalidAuthError
+import voluptuous as vol
+from yarl import URL
+
+from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN
+from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_SSL,
+    CONF_USERNAME,
+    CONF_VERIFY_SSL,
+)
+from homeassistant.helpers import selector
+from homeassistant.helpers.aiohttp_client import async_create_clientsession
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+
+from .const import (
+    CONF_DEEP_STANDBY,
+    CONF_SOURCE_BOUQUET,
+    CONF_USE_CHANNEL_ICON,
+    DEFAULT_PORT,
+    DEFAULT_SSL,
+    DEFAULT_VERIFY_SSL,
+    DOMAIN,
+)
+
+CONFIG_SCHEMA = vol.Schema(
+    {
+        vol.Required(CONF_HOST): selector.TextSelector(),
+        vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.All(
+            selector.NumberSelector(
+                selector.NumberSelectorConfig(
+                    min=1, max=65535, mode=selector.NumberSelectorMode.BOX
+                )
+            ),
+            vol.Coerce(int),
+        ),
+        vol.Optional(CONF_USERNAME): selector.TextSelector(),
+        vol.Optional(CONF_PASSWORD): selector.TextSelector(
+            selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
+        ),
+        vol.Required(CONF_SSL, default=DEFAULT_SSL): selector.BooleanSelector(),
+        vol.Required(
+            CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL
+        ): selector.BooleanSelector(),
+    }
+)
+
+
+class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for Enigma2."""
+
+    DATA_KEYS = (
+        CONF_HOST,
+        CONF_PORT,
+        CONF_USERNAME,
+        CONF_PASSWORD,
+        CONF_SSL,
+        CONF_VERIFY_SSL,
+    )
+    OPTIONS_KEYS = (CONF_DEEP_STANDBY, CONF_SOURCE_BOUQUET, CONF_USE_CHANNEL_ICON)
+
+    def __init__(self) -> None:
+        """Initialize the config flow."""
+        super().__init__()
+        self.errors: dict[str, str] = {}
+        self._data: dict[str, Any] = {}
+        self._options: dict[str, Any] = {}
+
+    async def validate_user_input(self, user_input: dict[str, Any]) -> dict[str, Any]:
+        """Validate user input."""
+
+        self.errors = {}
+
+        self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
+
+        base_url = URL.build(
+            scheme="http" if not user_input[CONF_SSL] else "https",
+            host=user_input[CONF_HOST],
+            port=user_input[CONF_PORT],
+            user=user_input.get(CONF_USERNAME),
+            password=user_input.get(CONF_PASSWORD),
+        )
+
+        session = async_create_clientsession(
+            self.hass, verify_ssl=user_input[CONF_VERIFY_SSL], base_url=base_url
+        )
+
+        try:
+            about = await OpenWebIfDevice(session).get_about()
+        except InvalidAuthError:
+            self.errors["base"] = "invalid_auth"
+        except ClientError:
+            self.errors["base"] = "cannot_connect"
+        except Exception:  # pylint: disable=broad-except
+            self.errors["base"] = "unknown"
+        else:
+            await self.async_set_unique_id(about["info"]["ifaces"][0]["mac"])
+            self._abort_if_unique_id_configured()
+
+        return user_input
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> ConfigFlowResult:
+        """Handle the user step."""
+        if user_input is None:
+            return self.async_show_form(step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA)
+
+        data = await self.validate_user_input(user_input)
+        if "base" in self.errors:
+            return self.async_show_form(
+                step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA, errors=self.errors
+            )
+        return self.async_create_entry(
+            data=data, title=data[CONF_HOST], options=self._options
+        )
+
+    async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
+        """Validate import."""
+        if CONF_PORT not in user_input:
+            user_input[CONF_PORT] = DEFAULT_PORT
+        if CONF_SSL not in user_input:
+            user_input[CONF_SSL] = DEFAULT_SSL
+        user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
+
+        async_create_issue(
+            self.hass,
+            HOMEASSISTANT_DOMAIN,
+            f"deprecated_yaml_{DOMAIN}",
+            breaks_in_ha_version="2024.11.0",
+            is_fixable=False,
+            is_persistent=False,
+            issue_domain=DOMAIN,
+            severity=IssueSeverity.WARNING,
+            translation_key="deprecated_yaml",
+            translation_placeholders={
+                "domain": DOMAIN,
+                "integration_title": "Enigma2",
+            },
+        )
+
+        self._data = {
+            key: user_input[key] for key in user_input if key in self.DATA_KEYS
+        }
+        self._options = {
+            key: user_input[key] for key in user_input if key in self.OPTIONS_KEYS
+        }
+
+        return await self.async_step_user(self._data)
diff --git a/homeassistant/components/enigma2/const.py b/homeassistant/components/enigma2/const.py
index 277efad50eb..d7508fee64e 100644
--- a/homeassistant/components/enigma2/const.py
+++ b/homeassistant/components/enigma2/const.py
@@ -16,3 +16,4 @@ DEFAULT_PASSWORD = "dreambox"
 DEFAULT_DEEP_STANDBY = False
 DEFAULT_SOURCE_BOUQUET = ""
 DEFAULT_MAC_ADDRESS = ""
+DEFAULT_VERIFY_SSL = False
diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json
index 0de4adc13b8..ef08314e541 100644
--- a/homeassistant/components/enigma2/manifest.json
+++ b/homeassistant/components/enigma2/manifest.json
@@ -2,7 +2,9 @@
   "domain": "enigma2",
   "name": "Enigma2 (OpenWebif)",
   "codeowners": ["@autinerd"],
+  "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/enigma2",
+  "integration_type": "device",
   "iot_class": "local_polling",
   "loggers": ["openwebif"],
   "requirements": ["openwebifpy==4.2.4"]
diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py
index afe8a426c72..037d82cd6c0 100644
--- a/homeassistant/components/enigma2/media_player.py
+++ b/homeassistant/components/enigma2/media_player.py
@@ -9,7 +9,6 @@ from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedEr
 from openwebif.api import OpenWebIfDevice
 from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
 import voluptuous as vol
-from yarl import URL
 
 from homeassistant.components.media_player import (
     MediaPlayerEntity,
@@ -17,6 +16,7 @@ from homeassistant.components.media_player import (
     MediaPlayerState,
     MediaType,
 )
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import (
     CONF_HOST,
     CONF_NAME,
@@ -26,10 +26,9 @@ from homeassistant.const import (
     CONF_USERNAME,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.aiohttp_client import async_create_clientsession
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
+from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
@@ -47,6 +46,7 @@ from .const import (
     DEFAULT_SSL,
     DEFAULT_USE_CHANNEL_ICON,
     DEFAULT_USERNAME,
+    DOMAIN,
 )
 
 ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording"
@@ -81,49 +81,44 @@ async def async_setup_platform(
     discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
     """Set up of an enigma2 media player."""
-    if discovery_info:
-        # Discovery gives us the streaming service port (8001)
-        # which is not useful as OpenWebif never runs on that port.
-        # So use the default port instead.
-        config[CONF_PORT] = DEFAULT_PORT
-        config[CONF_NAME] = discovery_info["hostname"]
-        config[CONF_HOST] = discovery_info["host"]
-        config[CONF_USERNAME] = DEFAULT_USERNAME
-        config[CONF_PASSWORD] = DEFAULT_PASSWORD
-        config[CONF_SSL] = DEFAULT_SSL
-        config[CONF_USE_CHANNEL_ICON] = DEFAULT_USE_CHANNEL_ICON
-        config[CONF_MAC_ADDRESS] = DEFAULT_MAC_ADDRESS
-        config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY
-        config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET
-
-    base_url = URL.build(
-        scheme="https" if config[CONF_SSL] else "http",
-        host=config[CONF_HOST],
-        port=config.get(CONF_PORT),
-        user=config.get(CONF_USERNAME),
-        password=config.get(CONF_PASSWORD),
-    )
 
-    session = async_create_clientsession(hass, verify_ssl=False, base_url=base_url)
+    entry_data = {
+        CONF_HOST: config[CONF_HOST],
+        CONF_PORT: config[CONF_PORT],
+        CONF_USERNAME: config[CONF_USERNAME],
+        CONF_PASSWORD: config[CONF_PASSWORD],
+        CONF_SSL: config[CONF_SSL],
+        CONF_USE_CHANNEL_ICON: config[CONF_USE_CHANNEL_ICON],
+        CONF_DEEP_STANDBY: config[CONF_DEEP_STANDBY],
+        CONF_SOURCE_BOUQUET: config[CONF_SOURCE_BOUQUET],
+    }
 
-    device = OpenWebIfDevice(
-        host=session,
-        turn_off_to_deep=config.get(CONF_DEEP_STANDBY, False),
-        source_bouquet=config.get(CONF_SOURCE_BOUQUET),
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_data
+        )
     )
 
-    try:
-        about = await device.get_about()
-    except ClientConnectorError as err:
-        raise PlatformNotReady from err
 
-    async_add_entities([Enigma2Device(config[CONF_NAME], device, about)])
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up the Enigma2 media player platform."""
+
+    device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id]
+    about = await device.get_about()
+    device.mac_address = about["info"]["ifaces"][0]["mac"]
+    entity = Enigma2Device(entry, device, about)
+    async_add_entities([entity])
 
 
 class Enigma2Device(MediaPlayerEntity):
     """Representation of an Enigma2 box."""
 
     _attr_has_entity_name = True
+    _attr_name = None
 
     _attr_media_content_type = MediaType.TVSHOW
     _attr_supported_features = (
@@ -139,14 +134,23 @@ class Enigma2Device(MediaPlayerEntity):
         | MediaPlayerEntityFeature.SELECT_SOURCE
     )
 
-    def __init__(self, name: str, device: OpenWebIfDevice, about: dict) -> None:
+    def __init__(
+        self, entry: ConfigEntry, device: OpenWebIfDevice, about: dict
+    ) -> None:
         """Initialize the Enigma2 device."""
         self._device: OpenWebIfDevice = device
-        self._device.mac_address = about["info"]["ifaces"][0]["mac"]
+        self._entry = entry
 
-        self._attr_name = name
         self._attr_unique_id = device.mac_address
 
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, device.mac_address)},
+            manufacturer=about["info"]["brand"],
+            model=about["info"]["model"],
+            configuration_url=device.base,
+            name=entry.data[CONF_HOST],
+        )
+
     async def async_turn_off(self) -> None:
         """Turn off media player."""
         if self._device.turn_off_to_deep:
diff --git a/homeassistant/components/enigma2/strings.json b/homeassistant/components/enigma2/strings.json
new file mode 100644
index 00000000000..888c6d59387
--- /dev/null
+++ b/homeassistant/components/enigma2/strings.json
@@ -0,0 +1,30 @@
+{
+  "config": {
+    "flow_title": "{name}",
+    "step": {
+      "user": {
+        "description": "Please enter the connection details of your device.",
+        "data": {
+          "host": "[%key:common::config_flow::data::host%]",
+          "port": "[%key:common::config_flow::data::port%]",
+          "ssl": "[%key:common::config_flow::data::ssl%]",
+          "username": "[%key:common::config_flow::data::username%]",
+          "password": "[%key:common::config_flow::data::password%]",
+          "name": "[%key:common::config_flow::data::name%]",
+          "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    }
+  }
+}
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index fd87c965db5..c02d8a2987e 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -148,6 +148,7 @@ FLOWS = {
         "emulated_roku",
         "energenie_power_sockets",
         "energyzero",
+        "enigma2",
         "enocean",
         "enphase_envoy",
         "environment_canada",
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index d10cb3fdb80..2b1e5b4fb91 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1604,8 +1604,8 @@
     },
     "enigma2": {
       "name": "Enigma2 (OpenWebif)",
-      "integration_type": "hub",
-      "config_flow": false,
+      "integration_type": "device",
+      "config_flow": true,
       "iot_class": "local_polling"
     },
     "enmax": {
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index ea115d4b29d..19aae180e4f 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1173,6 +1173,9 @@ openerz-api==0.3.0
 # homeassistant.components.openhome
 openhomedevice==2.2.0
 
+# homeassistant.components.enigma2
+openwebifpy==4.2.4
+
 # homeassistant.components.opower
 opower==0.4.3
 
diff --git a/tests/components/enigma2/__init__.py b/tests/components/enigma2/__init__.py
new file mode 100644
index 00000000000..15580d55b17
--- /dev/null
+++ b/tests/components/enigma2/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Enigma2 integration."""
diff --git a/tests/components/enigma2/conftest.py b/tests/components/enigma2/conftest.py
new file mode 100644
index 00000000000..9bbbda895bd
--- /dev/null
+++ b/tests/components/enigma2/conftest.py
@@ -0,0 +1,90 @@
+"""Test the Enigma2 config flow."""
+
+from homeassistant.components.enigma2.const import (
+    CONF_DEEP_STANDBY,
+    CONF_MAC_ADDRESS,
+    CONF_SOURCE_BOUQUET,
+    CONF_USE_CHANNEL_ICON,
+    DEFAULT_DEEP_STANDBY,
+    DEFAULT_PORT,
+    DEFAULT_SSL,
+    DEFAULT_VERIFY_SSL,
+)
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_NAME,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_SSL,
+    CONF_USERNAME,
+    CONF_VERIFY_SSL,
+)
+
+MAC_ADDRESS = "12:34:56:78:90:ab"
+
+TEST_REQUIRED = {
+    CONF_HOST: "1.1.1.1",
+    CONF_PORT: DEFAULT_PORT,
+    CONF_SSL: DEFAULT_SSL,
+    CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
+}
+
+TEST_FULL = {
+    CONF_HOST: "1.1.1.1",
+    CONF_PORT: DEFAULT_PORT,
+    CONF_SSL: DEFAULT_SSL,
+    CONF_USERNAME: "root",
+    CONF_PASSWORD: "password",
+    CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
+}
+
+TEST_IMPORT_FULL = {
+    CONF_HOST: "1.1.1.1",
+    CONF_PORT: DEFAULT_PORT,
+    CONF_SSL: DEFAULT_SSL,
+    CONF_USERNAME: "root",
+    CONF_PASSWORD: "password",
+    CONF_NAME: "My Player",
+    CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY,
+    CONF_SOURCE_BOUQUET: "Favourites",
+    CONF_MAC_ADDRESS: MAC_ADDRESS,
+    CONF_USE_CHANNEL_ICON: False,
+}
+
+TEST_IMPORT_REQUIRED = {CONF_HOST: "1.1.1.1"}
+
+EXPECTED_OPTIONS = {
+    CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY,
+    CONF_SOURCE_BOUQUET: "Favourites",
+    CONF_USE_CHANNEL_ICON: False,
+}
+
+
+class MockDevice:
+    """A mock Enigma2 device."""
+
+    mac_address: str | None = "12:34:56:78:90:ab"
+    _base = "http://1.1.1.1"
+
+    async def _call_api(self, url: str) -> dict:
+        if url.endswith("/api/about"):
+            return {
+                "info": {
+                    "ifaces": [
+                        {
+                            "mac": self.mac_address,
+                        }
+                    ]
+                }
+            }
+
+    def get_version(self):
+        """Return the version."""
+        return None
+
+    async def get_about(self) -> dict:
+        """Get mock about endpoint."""
+        return await self._call_api("/api/about")
+
+    async def close(self):
+        """Mock close."""
diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py
new file mode 100644
index 00000000000..dcd249ad943
--- /dev/null
+++ b/tests/components/enigma2/test_config_flow.py
@@ -0,0 +1,149 @@
+"""Test the Enigma2 config flow."""
+
+from typing import Any
+from unittest.mock import patch
+
+from aiohttp.client_exceptions import ClientError
+from openwebif.error import InvalidAuthError
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.enigma2.const import DOMAIN
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResultType
+
+from .conftest import (
+    EXPECTED_OPTIONS,
+    TEST_FULL,
+    TEST_IMPORT_FULL,
+    TEST_IMPORT_REQUIRED,
+    TEST_REQUIRED,
+    MockDevice,
+)
+
+
+@pytest.fixture
+async def user_flow(hass: HomeAssistant) -> str:
+    """Return a user-initiated flow after filling in host info."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+    assert result["type"] == FlowResultType.FORM
+    assert result["errors"] is None
+    return result["flow_id"]
+
+
+@pytest.mark.parametrize(
+    ("test_config"),
+    [(TEST_FULL), (TEST_REQUIRED)],
+)
+async def test_form_user(
+    hass: HomeAssistant, user_flow: str, test_config: dict[str, Any]
+):
+    """Test a successful user initiated flow."""
+    with (
+        patch(
+            "openwebif.api.OpenWebIfDevice.__new__",
+            return_value=MockDevice(),
+        ),
+        patch(
+            "homeassistant.components.enigma2.async_setup_entry",
+            return_value=True,
+        ) as mock_setup_entry,
+    ):
+        result = await hass.config_entries.flow.async_configure(user_flow, test_config)
+        await hass.async_block_till_done()
+    assert result["type"] == FlowResultType.CREATE_ENTRY
+    assert result["title"] == test_config[CONF_HOST]
+    assert result["data"] == test_config
+
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.parametrize(
+    ("exception", "error_type"),
+    [
+        (InvalidAuthError, "invalid_auth"),
+        (ClientError, "cannot_connect"),
+        (Exception, "unknown"),
+    ],
+)
+async def test_form_user_errors(
+    hass: HomeAssistant, user_flow, exception: Exception, error_type: str
+) -> None:
+    """Test we handle errors."""
+    with patch(
+        "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__",
+        side_effect=exception,
+    ):
+        result = await hass.config_entries.flow.async_configure(user_flow, TEST_FULL)
+
+    assert result["type"] == FlowResultType.FORM
+    assert result["step_id"] == config_entries.SOURCE_USER
+    assert result["errors"] == {"base": error_type}
+
+
+@pytest.mark.parametrize(
+    ("test_config", "expected_data", "expected_options"),
+    [
+        (TEST_IMPORT_FULL, TEST_FULL, EXPECTED_OPTIONS),
+        (TEST_IMPORT_REQUIRED, TEST_REQUIRED, {}),
+    ],
+)
+async def test_form_import(
+    hass: HomeAssistant,
+    test_config: dict[str, Any],
+    expected_data: dict[str, Any],
+    expected_options: dict[str, Any],
+) -> None:
+    """Test we get the form with import source."""
+    with (
+        patch(
+            "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__",
+            return_value=MockDevice(),
+        ),
+        patch(
+            "homeassistant.components.enigma2.async_setup_entry",
+            return_value=True,
+        ) as mock_setup_entry,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data=test_config,
+        )
+        await hass.async_block_till_done()
+
+    assert result["type"] == FlowResultType.CREATE_ENTRY
+    assert result["title"] == test_config[CONF_HOST]
+    assert result["data"] == expected_data
+    assert result["options"] == expected_options
+
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.parametrize(
+    ("exception", "error_type"),
+    [
+        (InvalidAuthError, "invalid_auth"),
+        (ClientError, "cannot_connect"),
+        (Exception, "unknown"),
+    ],
+)
+async def test_form_import_errors(
+    hass: HomeAssistant, exception: Exception, error_type: str
+) -> None:
+    """Test we handle errors on import."""
+    with patch(
+        "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__",
+        side_effect=exception,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data=TEST_IMPORT_FULL,
+        )
+
+    assert result["type"] == FlowResultType.FORM
+    assert result["errors"] == {"base": error_type}
diff --git a/tests/components/enigma2/test_init.py b/tests/components/enigma2/test_init.py
new file mode 100644
index 00000000000..93a130eef54
--- /dev/null
+++ b/tests/components/enigma2/test_init.py
@@ -0,0 +1,38 @@
+"""Test the Enigma2 integration init."""
+
+from unittest.mock import patch
+
+from homeassistant.components.enigma2.const import DOMAIN
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.core import HomeAssistant
+
+from .conftest import TEST_REQUIRED, MockDevice
+
+from tests.common import MockConfigEntry
+
+
+async def test_unload_entry(hass: HomeAssistant) -> None:
+    """Test successful unload of entry."""
+    with (
+        patch(
+            "homeassistant.components.enigma2.OpenWebIfDevice.__new__",
+            return_value=MockDevice(),
+        ),
+        patch(
+            "homeassistant.components.enigma2.media_player.async_setup_entry",
+            return_value=True,
+        ),
+    ):
+        entry = MockConfigEntry(domain=DOMAIN, data=TEST_REQUIRED, title="name")
+        entry.add_to_hass(hass)
+        await hass.config_entries.async_setup(entry.entry_id)
+        await hass.async_block_till_done()
+
+    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+    assert entry.state is ConfigEntryState.LOADED
+
+    assert await hass.config_entries.async_unload(entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert entry.state is ConfigEntryState.NOT_LOADED
+    assert not hass.data.get(DOMAIN)
-- 
GitLab