From b08832a89aacc83a2d3e1dfce00d9bf709781d54 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 11 Jan 2024 08:27:15 +0100 Subject: [PATCH] Fastdotcom service optimization (#107179) * Startup mechanic * Workable service (again) * Optimized version, for now * Minor refactoring * Test cases * Fixing test case * Adding startup comment * State_unknown added * Update homeassistant/components/fastdotcom/services.py Co-authored-by: Martin Hjelmare * Check if config entries are not found * Update tests/components/fastdotcom/test_service.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/fastdotcom/services.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/fastdotcom/services.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- .../components/fastdotcom/__init__.py | 43 +++------ homeassistant/components/fastdotcom/const.py | 2 + .../components/fastdotcom/services.py | 51 +++++++++++ tests/components/fastdotcom/test_init.py | 16 ++-- tests/components/fastdotcom/test_service.py | 87 +++++++++++++++++++ 5 files changed, 161 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/fastdotcom/services.py create mode 100644 tests/components/fastdotcom/test_service.py diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index 165d81edd0b..ada717a6dac 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -6,14 +6,15 @@ import logging import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall -from homeassistant.helpers import issue_registry as ir +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from .const import CONF_MANUAL, DEFAULT_INTERVAL, DOMAIN, PLATFORMS from .coordinator import FastdotcomDataUpdateCoordindator +from .services import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -33,7 +34,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Fast.com component. (deprecated).""" + """Set up the Fastdotcom component.""" if DOMAIN in config: hass.async_create_task( hass.config_entries.flow.async_init( @@ -42,51 +43,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data=config[DOMAIN], ) ) + async_setup_services(hass) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fast.com from a config entry.""" coordinator = FastdotcomDataUpdateCoordindator(hass) - - async def _request_refresh(event: Event) -> None: - """Request a refresh.""" - await coordinator.async_request_refresh() - - async def _request_refresh_service(call: ServiceCall) -> None: - """Request a refresh via the service.""" - ir.async_create_issue( - hass, - DOMAIN, - "service_deprecation", - breaks_in_ha_version="2024.7.0", - is_fixable=True, - is_persistent=False, - severity=ir.IssueSeverity.WARNING, - translation_key="service_deprecation", - ) - await coordinator.async_request_refresh() - - if hass.state == CoreState.running: - await coordinator.async_config_entry_first_refresh() - else: - # Don't start the speedtest when HA is starting up - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _request_refresh) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.services.async_register(DOMAIN, "speedtest", _request_refresh_service) await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS, ) + async def _async_finish_startup(hass: HomeAssistant) -> None: + """Run this only when HA has finished its startup.""" + await coordinator.async_config_entry_first_refresh() + + # Don't start a speedtest during startup, this will slow down the overall startup dramatically + async_at_started(hass, _async_finish_startup) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Fast.com config entry.""" - hass.services.async_remove(DOMAIN, "speedtest") 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/fastdotcom/const.py b/homeassistant/components/fastdotcom/const.py index 753825c4361..340be6f50ae 100644 --- a/homeassistant/components/fastdotcom/const.py +++ b/homeassistant/components/fastdotcom/const.py @@ -10,6 +10,8 @@ DATA_UPDATED = f"{DOMAIN}_data_updated" CONF_MANUAL = "manual" +SERVICE_NAME = "speedtest" + DEFAULT_NAME = "Fast.com" DEFAULT_INTERVAL = 1 PLATFORMS: list[Platform] = [Platform.SENSOR] diff --git a/homeassistant/components/fastdotcom/services.py b/homeassistant/components/fastdotcom/services.py new file mode 100644 index 00000000000..d1a9ee2125b --- /dev/null +++ b/homeassistant/components/fastdotcom/services.py @@ -0,0 +1,51 @@ +"""Services for the Fastdotcom integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir + +from .const import DOMAIN, SERVICE_NAME +from .coordinator import FastdotcomDataUpdateCoordindator + + +def async_setup_services(hass: HomeAssistant) -> None: + """Set up the service for the Fastdotcom integration.""" + + @callback + def collect_coordinator() -> FastdotcomDataUpdateCoordindator: + """Collect the coordinator Fastdotcom.""" + config_entries = hass.config_entries.async_entries(DOMAIN) + if not config_entries: + raise HomeAssistantError("No Fast.com config entries found") + + for config_entry in config_entries: + if config_entry.state != ConfigEntryState.LOADED: + raise HomeAssistantError(f"{config_entry.title} is not loaded") + coordinator: FastdotcomDataUpdateCoordindator = hass.data[DOMAIN][ + config_entry.entry_id + ] + break + return coordinator + + async def async_perform_service(call: ServiceCall) -> None: + """Perform a service call to manually run Fastdotcom.""" + ir.async_create_issue( + hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.7.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) + coordinator = collect_coordinator() + await coordinator.async_request_refresh() + + hass.services.async_register( + DOMAIN, + SERVICE_NAME, + async_perform_service, + ) diff --git a/tests/components/fastdotcom/test_init.py b/tests/components/fastdotcom/test_init.py index 0acaddf36fc..dc61acb620e 100644 --- a/tests/components/fastdotcom/test_init.py +++ b/tests/components/fastdotcom/test_init.py @@ -65,23 +65,25 @@ async def test_delayed_speedtest_during_startup( config_entry.add_to_hass(hass) with patch( - "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 + "homeassistant.components.fastdotcom.coordinator.fast_com" ), patch.object(hass, "state", CoreState.starting): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state == config_entries.ConfigEntryState.LOADED state = hass.states.get("sensor.fast_com_download") - assert state is not None - # Assert state is unknown as coordinator is not allowed to start and fetch data yet - assert state.state == STATE_UNKNOWN + # Assert state is Unknown as fast.com isn't starting until HA has started + assert state.state is STATE_UNKNOWN - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() state = hass.states.get("sensor.fast_com_download") assert state is not None - assert state.state == "0" + assert state.state == "5.0" assert config_entry.state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/fastdotcom/test_service.py b/tests/components/fastdotcom/test_service.py new file mode 100644 index 00000000000..2f919bc8a84 --- /dev/null +++ b/tests/components/fastdotcom/test_service.py @@ -0,0 +1,87 @@ +"""Test Fastdotcom service.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.fastdotcom.const import DEFAULT_NAME, DOMAIN, SERVICE_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + + +async def test_service(hass: HomeAssistant) -> None: + """Test the Fastdotcom service.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="UNIQUE_TEST_ID", + title=DEFAULT_NAME, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=0 + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fast_com_download") + assert state is not None + assert state.state == "0" + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=5.0 + ): + await hass.services.async_call(DOMAIN, SERVICE_NAME, blocking=True) + + state = hass.states.get("sensor.fast_com_download") + assert state is not None + assert state.state == "5.0" + + +async def test_service_unloaded_entry(hass: HomeAssistant) -> None: + """Test service called when config entry unloaded.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="UNIQUE_TEST_ID", + title=DEFAULT_NAME, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=0 + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry + await config_entry.async_unload(hass) + + with pytest.raises(HomeAssistantError) as exc: + await hass.services.async_call(DOMAIN, SERVICE_NAME, blocking=True) + + assert "Fast.com is not loaded" in str(exc) + + +async def test_service_removed_entry(hass: HomeAssistant) -> None: + """Test service called when config entry was removed and HA was not restarted yet.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="UNIQUE_TEST_ID", + title=DEFAULT_NAME, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=0 + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry + await hass.config_entries.async_remove(config_entry.entry_id) + + with pytest.raises(HomeAssistantError) as exc: + await hass.services.async_call(DOMAIN, SERVICE_NAME, blocking=True) + + assert "No Fast.com config entries found" in str(exc)