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