From eb5c7a3e766b828338019d06c8b59401bdc4e962 Mon Sep 17 00:00:00 2001
From: Erwin Douna <e.douna@gmail.com>
Date: Tue, 21 Nov 2023 09:59:34 +0100
Subject: [PATCH] Add Fastdotcom config flow (#98686)

* Adding config flow and tests

* Removing update and adding to integrations.json

* Updating hassfest

* Removing comments

* Removing unique ID

* Putting the setup_platform out of order

* Adding feedback on issues and importing

* Removing uniqueID (again)

* Adjusting unload and typo

* Updating manifest properly

* Minor patching

* Removing hass.data.setdefault(DOMAIN, {})

* Moving load_platform to __init__.py

* Update homeassistant/components/fastdotcom/config_flow.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/fastdotcom/strings.json

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/fastdotcom/__init__.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Update homeassistant/components/fastdotcom/config_flow.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Adding an unload function for the timer

* Adding issue on setup platform in sensor

* Update homeassistant/components/fastdotcom/config_flow.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Removing platform

* Fixing strings.json

* Fine-tuning

* Putting back last_state

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
---
 .coveragerc                                   |  3 +-
 CODEOWNERS                                    |  3 +-
 .../components/fastdotcom/__init__.py         | 60 +++++++++------
 .../components/fastdotcom/config_flow.py      | 50 +++++++++++++
 homeassistant/components/fastdotcom/const.py  | 15 ++++
 .../components/fastdotcom/manifest.json       |  3 +-
 homeassistant/components/fastdotcom/sensor.py | 13 ++--
 .../components/fastdotcom/strings.json        | 10 +++
 homeassistant/generated/config_flows.py       |  1 +
 homeassistant/generated/integrations.json     |  2 +-
 requirements_test_all.txt                     |  3 +
 tests/components/fastdotcom/__init__.py       |  1 +
 .../components/fastdotcom/test_config_flow.py | 74 +++++++++++++++++++
 13 files changed, 206 insertions(+), 32 deletions(-)
 create mode 100644 homeassistant/components/fastdotcom/config_flow.py
 create mode 100644 homeassistant/components/fastdotcom/const.py
 create mode 100644 tests/components/fastdotcom/__init__.py
 create mode 100644 tests/components/fastdotcom/test_config_flow.py

diff --git a/.coveragerc b/.coveragerc
index a05cc48785e..cdb5bdd07e4 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -368,7 +368,8 @@ omit =
     homeassistant/components/faa_delays/binary_sensor.py
     homeassistant/components/faa_delays/coordinator.py
     homeassistant/components/familyhub/camera.py
-    homeassistant/components/fastdotcom/*
+    homeassistant/components/fastdotcom/sensor.py
+    homeassistant/components/fastdotcom/__init__.py
     homeassistant/components/ffmpeg/camera.py
     homeassistant/components/fibaro/__init__.py
     homeassistant/components/fibaro/binary_sensor.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 25f8702ab5a..72c58942abc 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -373,7 +373,8 @@ build.json @home-assistant/supervisor
 /tests/components/faa_delays/ @ntilley905
 /homeassistant/components/fan/ @home-assistant/core
 /tests/components/fan/ @home-assistant/core
-/homeassistant/components/fastdotcom/ @rohankapoorcom
+/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
+/tests/components/fastdotcom/ @rohankapoorcom @erwindouna
 /homeassistant/components/fibaro/ @rappenze
 /tests/components/fibaro/ @rappenze
 /homeassistant/components/file/ @fabaff
diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py
index 50e0cb04869..2fe5b3ccafc 100644
--- a/homeassistant/components/fastdotcom/__init__.py
+++ b/homeassistant/components/fastdotcom/__init__.py
@@ -8,23 +8,18 @@ from typing import Any
 from fastdotcom import fast_com
 import voluptuous as vol
 
-from homeassistant.const import CONF_SCAN_INTERVAL, Platform
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import CONF_SCAN_INTERVAL
 from homeassistant.core import HomeAssistant, ServiceCall
 import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.discovery import async_load_platform
 from homeassistant.helpers.dispatcher import dispatcher_send
 from homeassistant.helpers.event import async_track_time_interval
 from homeassistant.helpers.typing import ConfigType
 
-DOMAIN = "fastdotcom"
-DATA_UPDATED = f"{DOMAIN}_data_updated"
+from .const import CONF_MANUAL, DATA_UPDATED, DEFAULT_INTERVAL, DOMAIN, PLATFORMS
 
 _LOGGER = logging.getLogger(__name__)
 
-CONF_MANUAL = "manual"
-
-DEFAULT_INTERVAL = timedelta(hours=1)
-
 CONFIG_SCHEMA = vol.Schema(
     {
         DOMAIN: vol.Schema(
@@ -40,38 +35,61 @@ CONFIG_SCHEMA = vol.Schema(
 )
 
 
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+async def async_setup_platform(hass: HomeAssistant, config: ConfigType) -> bool:
+    """Set up the Fast.com component. (deprecated)."""
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": SOURCE_IMPORT},
+            data=config[DOMAIN],
+        )
+    )
+    return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Set up the Fast.com component."""
-    conf = config[DOMAIN]
     data = hass.data[DOMAIN] = SpeedtestData(hass)
 
-    if not conf[CONF_MANUAL]:
-        async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL])
+    entry.async_on_unload(
+        async_track_time_interval(hass, data.update, timedelta(hours=DEFAULT_INTERVAL))
+    )
+    # Run an initial update to get a starting state
+    await data.update()
 
-    def update(service_call: ServiceCall | None = None) -> None:
+    async def update(service_call: ServiceCall | None = None) -> None:
         """Service call to manually update the data."""
-        data.update()
+        await data.update()
 
     hass.services.async_register(DOMAIN, "speedtest", update)
 
-    hass.async_create_task(
-        async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
+    await hass.config_entries.async_forward_entry_setups(
+        entry,
+        PLATFORMS,
     )
 
     return True
 
 
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload Fast.com config entry."""
+    if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+        hass.data.pop(DOMAIN)
+    return unload_ok
+
+
 class SpeedtestData:
-    """Get the latest data from fast.com."""
+    """Get the latest data from Fast.com."""
 
     def __init__(self, hass: HomeAssistant) -> None:
         """Initialize the data object."""
         self.data: dict[str, Any] | None = None
         self._hass = hass
 
-    def update(self, now: datetime | None = None) -> None:
+    async def update(self, now: datetime | None = None) -> None:
         """Get the latest data from fast.com."""
-
-        _LOGGER.debug("Executing fast.com speedtest")
-        self.data = {"download": fast_com()}
+        _LOGGER.debug("Executing Fast.com speedtest")
+        fast_com_data = await self._hass.async_add_executor_job(fast_com)
+        self.data = {"download": fast_com_data}
+        _LOGGER.debug("Fast.com speedtest finished, with mbit/s: %s", fast_com_data)
         dispatcher_send(self._hass, DATA_UPDATED)
diff --git a/homeassistant/components/fastdotcom/config_flow.py b/homeassistant/components/fastdotcom/config_flow.py
new file mode 100644
index 00000000000..5ca35fd6802
--- /dev/null
+++ b/homeassistant/components/fastdotcom/config_flow.py
@@ -0,0 +1,50 @@
+"""Config flow for Fast.com integration."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+
+from .const import DEFAULT_NAME, DOMAIN
+
+
+class FastdotcomConfigFlow(ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for Fast.com."""
+
+    VERSION = 1
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle the initial step."""
+        if self._async_current_entries():
+            return self.async_abort(reason="single_instance_allowed")
+
+        if user_input is not None:
+            return self.async_create_entry(title=DEFAULT_NAME, data={})
+
+        return self.async_show_form(step_id="user")
+
+    async def async_step_import(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle a flow initiated by configuration file."""
+        async_create_issue(
+            self.hass,
+            HOMEASSISTANT_DOMAIN,
+            f"deprecated_yaml_{DOMAIN}",
+            breaks_in_ha_version="2024.6.0",
+            is_fixable=False,
+            issue_domain=DOMAIN,
+            severity=IssueSeverity.WARNING,
+            translation_key="deprecated_yaml",
+            translation_placeholders={
+                "domain": DOMAIN,
+                "integration_title": "Fast.com",
+            },
+        )
+
+        return await self.async_step_user(user_input)
diff --git a/homeassistant/components/fastdotcom/const.py b/homeassistant/components/fastdotcom/const.py
new file mode 100644
index 00000000000..753825c4361
--- /dev/null
+++ b/homeassistant/components/fastdotcom/const.py
@@ -0,0 +1,15 @@
+"""Constants for the Fast.com integration."""
+import logging
+
+from homeassistant.const import Platform
+
+LOGGER = logging.getLogger(__package__)
+
+DOMAIN = "fastdotcom"
+DATA_UPDATED = f"{DOMAIN}_data_updated"
+
+CONF_MANUAL = "manual"
+
+DEFAULT_NAME = "Fast.com"
+DEFAULT_INTERVAL = 1
+PLATFORMS: list[Platform] = [Platform.SENSOR]
diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json
index 73db5c0bf11..02fd3ade205 100644
--- a/homeassistant/components/fastdotcom/manifest.json
+++ b/homeassistant/components/fastdotcom/manifest.json
@@ -1,7 +1,8 @@
 {
   "domain": "fastdotcom",
   "name": "Fast.com",
-  "codeowners": ["@rohankapoorcom"],
+  "codeowners": ["@rohankapoorcom", "@erwindouna"],
+  "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/fastdotcom",
   "iot_class": "cloud_polling",
   "loggers": ["fastdotcom"],
diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py
index b20b0213835..33ad4853404 100644
--- a/homeassistant/components/fastdotcom/sensor.py
+++ b/homeassistant/components/fastdotcom/sensor.py
@@ -8,29 +8,28 @@ from homeassistant.components.sensor import (
     SensorEntity,
     SensorStateClass,
 )
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import UnitOfDataRate
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.restore_state import RestoreEntity
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
-from . import DATA_UPDATED, DOMAIN as FASTDOTCOM_DOMAIN
+from .const import DATA_UPDATED, DOMAIN
 
 
-async def async_setup_platform(
+async def async_setup_entry(
     hass: HomeAssistant,
-    config: ConfigType,
+    entry: ConfigEntry,
     async_add_entities: AddEntitiesCallback,
-    discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
     """Set up the Fast.com sensor."""
-    async_add_entities([SpeedtestSensor(hass.data[FASTDOTCOM_DOMAIN])])
+    async_add_entities([SpeedtestSensor(hass.data[DOMAIN])])
 
 
 # pylint: disable-next=hass-invalid-inheritance # needs fixing
 class SpeedtestSensor(RestoreEntity, SensorEntity):
-    """Implementation of a FAst.com sensor."""
+    """Implementation of a Fast.com sensor."""
 
     _attr_name = "Fast.com Download"
     _attr_device_class = SensorDeviceClass.DATA_RATE
diff --git a/homeassistant/components/fastdotcom/strings.json b/homeassistant/components/fastdotcom/strings.json
index 705eada9387..d647250b423 100644
--- a/homeassistant/components/fastdotcom/strings.json
+++ b/homeassistant/components/fastdotcom/strings.json
@@ -1,4 +1,14 @@
 {
+  "config": {
+    "step": {
+      "user": {
+        "description": "Do you want to start the setup? The initial setup will take about 30-40 seconds."
+      }
+    },
+    "abort": {
+      "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+    }
+  },
   "services": {
     "speedtest": {
       "name": "Speed test",
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 9c77bd753f8..d5a5176a974 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -141,6 +141,7 @@ FLOWS = {
         "evil_genius_labs",
         "ezviz",
         "faa_delays",
+        "fastdotcom",
         "fibaro",
         "filesize",
         "fireservicerota",
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index bdc12cceb8e..ec35b83b630 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1656,7 +1656,7 @@
     "fastdotcom": {
       "name": "Fast.com",
       "integration_type": "hub",
-      "config_flow": false,
+      "config_flow": true,
       "iot_class": "cloud_polling"
     },
     "feedreader": {
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 0ed5bdefd4a..5f41b0056ef 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -634,6 +634,9 @@ eufylife-ble-client==0.1.8
 # homeassistant.components.faa_delays
 faadelays==2023.9.1
 
+# homeassistant.components.fastdotcom
+fastdotcom==0.0.3
+
 # homeassistant.components.feedreader
 feedparser==6.0.10
 
diff --git a/tests/components/fastdotcom/__init__.py b/tests/components/fastdotcom/__init__.py
new file mode 100644
index 00000000000..4c2ca6301af
--- /dev/null
+++ b/tests/components/fastdotcom/__init__.py
@@ -0,0 +1 @@
+"""Fast.com integration tests."""
diff --git a/tests/components/fastdotcom/test_config_flow.py b/tests/components/fastdotcom/test_config_flow.py
new file mode 100644
index 00000000000..4314a7688d8
--- /dev/null
+++ b/tests/components/fastdotcom/test_config_flow.py
@@ -0,0 +1,74 @@
+"""Test for the Fast.com config flow."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.fastdotcom.const import DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResultType
+
+from tests.common import MockConfigEntry
+
+
+async def test_user_form(hass: HomeAssistant) -> None:
+    """Test the full user configuration flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": SOURCE_USER}
+    )
+
+    assert result["type"] == FlowResultType.FORM
+    assert result["step_id"] == "user"
+
+    with patch(
+        "homeassistant.components.fastdotcom.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            user_input={},
+        )
+
+    assert result["type"] == FlowResultType.CREATE_ENTRY
+    assert result["title"] == "Fast.com"
+    assert result["data"] == {}
+    assert result["options"] == {}
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT])
+async def test_single_instance_allowed(
+    hass: HomeAssistant,
+    source: str,
+) -> None:
+    """Test we abort if already setup."""
+    mock_config_entry = MockConfigEntry(domain=DOMAIN)
+
+    mock_config_entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": source}
+    )
+
+    assert result["type"] == FlowResultType.ABORT
+    assert result["reason"] == "single_instance_allowed"
+
+
+async def test_import_flow_success(hass: HomeAssistant) -> None:
+    """Test import flow."""
+    with patch(
+        "homeassistant.components.fastdotcom.__init__.SpeedtestData",
+        return_value={"download": "50"},
+    ), patch("homeassistant.components.fastdotcom.sensor.SpeedtestSensor"):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data={},
+        )
+        await hass.async_block_till_done()
+
+        assert result["type"] == FlowResultType.CREATE_ENTRY
+        assert result["title"] == "Fast.com"
+        assert result["data"] == {}
+        assert result["options"] == {}
-- 
GitLab