diff --git a/.coveragerc b/.coveragerc index a05cc48785e879cf6e298f4069840ea1177aada2..cdb5bdd07e487db695b0e1b4d06c090200102fa8 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 25f8702ab5a3ccc6965480e4c28660717e7f5d71..72c58942abc7fa6b29be6edb8e15dfbed98971b4 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 50e0cb0486958126e4930451db5cfca223be34b5..2fe5b3ccafc6a51f9fde101a2a1fb1f0d9fe9854 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 0000000000000000000000000000000000000000..5ca35fd6802bcdf19f255e03cb6cd684c1f9c3e7 --- /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 0000000000000000000000000000000000000000..753825c4361a351274f9c56f79d43e1baeeeecc1 --- /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 73db5c0bf1197f606485cbf5fa251bbb3b545425..02fd3ade2055039f12bdbda87af96091dc5574bc 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 b20b02138350800bede4a13e7c1b0a1599724d0f..33ad4853404f180ee0904efc9189cbfbba49d620 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 705eada93875e9e674c75b3a7e0ffe25def4d748..d647250b4239230359403e51f35eff68d5274ce3 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 9c77bd753f884e68e8d42695a93e25b40d8159cb..d5a5176a974442ef3bc745cca422960637e068d3 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 bdc12cceb8ef52c63dfd8f7791cf902d79c81c83..ec35b83b630649a97d1420b563c27a76f7104d86 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 0ed5bdefd4ae2e95a4027a2127d66944d9e42603..5f41b0056ef77a12871e100fe23892c102a2a0eb 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 0000000000000000000000000000000000000000..4c2ca6301afacd36bba34d181e5f47cac316f3e9 --- /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 0000000000000000000000000000000000000000..4314a7688d8edd17cd0da3524c30eebddfc542a8 --- /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"] == {}