Speedtestdotnet code cleanup and type hints (#52533)

This commit is contained in:
Rami Mosleh 2021-07-22 13:25:54 +03:00 committed by GitHub
parent 7768f53281
commit 1a450c2084
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 287 additions and 237 deletions

View file

@ -969,7 +969,6 @@ omit =
homeassistant/components/sonos/* homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/* homeassistant/components/spc/*
homeassistant/components/speedtestdotnet/*
homeassistant/components/spider/* homeassistant/components/spider/*
homeassistant/components/splunk/* homeassistant/components/splunk/*
homeassistant/components/spotify/__init__.py homeassistant/components/spotify/__init__.py

View file

@ -1,19 +1,22 @@
"""Support for testing internet speed via Speedtest.net.""" """Support for testing internet speed via Speedtest.net."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
import speedtest import speedtest
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
) )
from homeassistant.core import CoreState, callback from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
@ -22,6 +25,7 @@ from .const import (
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DEFAULT_SERVER, DEFAULT_SERVER,
DOMAIN, DOMAIN,
PLATFORMS,
SENSOR_TYPES, SENSOR_TYPES,
SPEED_TEST_SERVICE, SPEED_TEST_SERVICE,
) )
@ -51,10 +55,8 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
PLATFORMS = ["sensor"]
def server_id_valid(server_id: str) -> bool:
def server_id_valid(server_id):
"""Check if server_id is valid.""" """Check if server_id is valid."""
try: try:
api = speedtest.Speedtest() api = speedtest.Speedtest()
@ -65,7 +67,7 @@ def server_id_valid(server_id):
return True return True
async def async_setup(hass, config): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Import integration from config.""" """Import integration from config."""
if DOMAIN in config: if DOMAIN in config:
hass.async_create_task( hass.async_create_task(
@ -76,7 +78,7 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Speedtest.net component.""" """Set up the Speedtest.net component."""
coordinator = SpeedTestDataCoordinator(hass, config_entry) coordinator = SpeedTestDataCoordinator(hass, config_entry)
await coordinator.async_setup() await coordinator.async_setup()
@ -88,11 +90,9 @@ async def async_setup_entry(hass, config_entry):
) )
await coordinator.async_refresh() await coordinator.async_refresh()
if not config_entry.options[CONF_MANUAL]: if not config_entry.options.get(CONF_MANUAL, False):
if hass.state == CoreState.running: if hass.state == CoreState.running:
await _enable_scheduled_speedtests() await _enable_scheduled_speedtests()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
else: else:
# Running a speed test during startup can prevent # Running a speed test during startup can prevent
# integrations from being able to setup because it # integrations from being able to setup because it
@ -108,12 +108,10 @@ async def async_setup_entry(hass, config_entry):
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload SpeedTest Entry from config_entry.""" """Unload SpeedTest Entry from config_entry."""
hass.services.async_remove(DOMAIN, SPEED_TEST_SERVICE) hass.services.async_remove(DOMAIN, SPEED_TEST_SERVICE)
hass.data[DOMAIN].async_unload()
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS config_entry, PLATFORMS
) )
@ -125,13 +123,12 @@ async def async_unload_entry(hass, config_entry):
class SpeedTestDataCoordinator(DataUpdateCoordinator): class SpeedTestDataCoordinator(DataUpdateCoordinator):
"""Get the latest data from speedtest.net.""" """Get the latest data from speedtest.net."""
def __init__(self, hass, config_entry): def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the data object.""" """Initialize the data object."""
self.hass = hass self.hass = hass
self.config_entry = config_entry self.config_entry: ConfigEntry = config_entry
self.api = None self.api: speedtest.Speedtest | None = None
self.servers = {} self.servers: dict[str, dict] = {DEFAULT_SERVER: {}}
self._unsub_update_listener = None
super().__init__( super().__init__(
self.hass, self.hass,
_LOGGER, _LOGGER,
@ -141,51 +138,49 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
def update_servers(self): def update_servers(self):
"""Update list of test servers.""" """Update list of test servers."""
try: test_servers = self.api.get_servers()
server_list = self.api.get_servers() test_servers_list = []
except speedtest.ConfigRetrievalError: for servers in test_servers.values():
_LOGGER.debug("Error retrieving server list") for server in servers:
return test_servers_list.append(server)
if test_servers_list:
self.servers[DEFAULT_SERVER] = {} for server in sorted(
for server in sorted( test_servers_list,
server_list.values(), key=lambda server: (
key=lambda server: server[0]["country"] + server[0]["sponsor"], server["country"],
): server["name"],
self.servers[ server["sponsor"],
f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}" ),
] = server[0] ):
self.servers[
f"{server['country']} - {server['sponsor']} - {server['name']}"
] = server
def update_data(self): def update_data(self):
"""Get the latest data from speedtest.net.""" """Get the latest data from speedtest.net."""
self.update_servers() self.update_servers()
self.api.closest.clear() self.api.closest.clear()
if self.config_entry.options.get(CONF_SERVER_ID): if self.config_entry.options.get(CONF_SERVER_ID):
server_id = self.config_entry.options.get(CONF_SERVER_ID) server_id = self.config_entry.options.get(CONF_SERVER_ID)
self.api.get_servers(servers=[server_id]) self.api.get_servers(servers=[server_id])
try: best_server = self.api.get_best_server()
self.api.get_best_server()
except speedtest.SpeedtestBestServerFailure as err:
raise UpdateFailed(
"Failed to retrieve best server for speedtest", err
) from err
_LOGGER.debug( _LOGGER.debug(
"Executing speedtest.net speed test with server_id: %s", "Executing speedtest.net speed test with server_id: %s",
self.api.best["id"], best_server["id"],
) )
self.api.download() self.api.download()
self.api.upload() self.api.upload()
return self.api.results.dict() return self.api.results.dict()
async def async_update(self, *_): async def async_update(self) -> dict[str, str]:
"""Update Speedtest data.""" """Update Speedtest data."""
try: try:
return await self.hass.async_add_executor_job(self.update_data) return await self.hass.async_add_executor_job(self.update_data)
except (speedtest.ConfigRetrievalError, speedtest.NoMatchedServers) as err: except speedtest.NoMatchedServers as err:
raise UpdateFailed from err raise UpdateFailed("Selected server is not found.") from err
except speedtest.SpeedtestException as err:
raise UpdateFailed(err) from err
async def async_set_options(self): async def async_set_options(self):
"""Set options for entry.""" """Set options for entry."""
@ -200,11 +195,12 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
self.config_entry, data=data, options=options self.config_entry, data=data, options=options
) )
async def async_setup(self): async def async_setup(self) -> None:
"""Set up SpeedTest.""" """Set up SpeedTest."""
try: try:
self.api = await self.hass.async_add_executor_job(speedtest.Speedtest) self.api = await self.hass.async_add_executor_job(speedtest.Speedtest)
except speedtest.ConfigRetrievalError as err: await self.hass.async_add_executor_job(self.update_servers)
except speedtest.SpeedtestException as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
async def request_update(call): async def request_update(call):
@ -213,24 +209,14 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
await self.async_set_options() await self.async_set_options()
await self.hass.async_add_executor_job(self.update_servers)
self.hass.services.async_register(DOMAIN, SPEED_TEST_SERVICE, request_update) self.hass.services.async_register(DOMAIN, SPEED_TEST_SERVICE, request_update)
self._unsub_update_listener = self.config_entry.add_update_listener( self.config_entry.async_on_unload(
options_updated_listener self.config_entry.add_update_listener(options_updated_listener)
) )
@callback
def async_unload(self):
"""Unload the coordinator."""
if not self._unsub_update_listener:
return
self._unsub_update_listener()
self._unsub_update_listener = None
async def options_updated_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def options_updated_listener(hass, entry):
"""Handle options update.""" """Handle options update."""
if entry.options[CONF_MANUAL]: if entry.options[CONF_MANUAL]:
hass.data[DOMAIN].update_interval = None hass.data[DOMAIN].update_interval = None

View file

@ -1,9 +1,14 @@
"""Config flow for Speedtest.net.""" """Config flow for Speedtest.net."""
from __future__ import annotations
from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from . import server_id_valid from . import server_id_valid
from .const import ( from .const import (
@ -24,11 +29,15 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return SpeedTestOptionsFlowHandler(config_entry) return SpeedTestOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
@ -59,14 +68,16 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class SpeedTestOptionsFlowHandler(config_entries.OptionsFlow): class SpeedTestOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle SpeedTest options.""" """Handle SpeedTest options."""
def __init__(self, config_entry): def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self._servers = {} self._servers: dict = {}
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options.""" """Manage the options."""
errors = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
server_name = user_input[CONF_SERVER_NAME] server_name = user_input[CONF_SERVER_NAME]

View file

@ -1,32 +1,35 @@
"""Consts used by Speedtest.net.""" """Consts used by Speedtest.net."""
from typing import Final
from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND, TIME_MILLISECONDS from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND, TIME_MILLISECONDS
DOMAIN = "speedtestdotnet" DOMAIN: Final = "speedtestdotnet"
SPEED_TEST_SERVICE = "speedtest" SPEED_TEST_SERVICE: Final = "speedtest"
DATA_UPDATED = f"{DOMAIN}_data_updated"
SENSOR_TYPES = { SENSOR_TYPES: Final = {
"ping": ["Ping", TIME_MILLISECONDS], "ping": ["Ping", TIME_MILLISECONDS],
"download": ["Download", DATA_RATE_MEGABITS_PER_SECOND], "download": ["Download", DATA_RATE_MEGABITS_PER_SECOND],
"upload": ["Upload", DATA_RATE_MEGABITS_PER_SECOND], "upload": ["Upload", DATA_RATE_MEGABITS_PER_SECOND],
} }
CONF_SERVER_NAME = "server_name" CONF_SERVER_NAME: Final = "server_name"
CONF_SERVER_ID = "server_id" CONF_SERVER_ID: Final = "server_id"
CONF_MANUAL = "manual" CONF_MANUAL: Final = "manual"
ATTR_BYTES_RECEIVED = "bytes_received" ATTR_BYTES_RECEIVED: Final = "bytes_received"
ATTR_BYTES_SENT = "bytes_sent" ATTR_BYTES_SENT: Final = "bytes_sent"
ATTR_SERVER_COUNTRY = "server_country" ATTR_SERVER_COUNTRY: Final = "server_country"
ATTR_SERVER_ID = "server_id" ATTR_SERVER_ID: Final = "server_id"
ATTR_SERVER_NAME = "server_name" ATTR_SERVER_NAME: Final = "server_name"
DEFAULT_NAME = "SpeedTest" DEFAULT_NAME: Final = "SpeedTest"
DEFAULT_SCAN_INTERVAL = 60 DEFAULT_SCAN_INTERVAL: Final = 60
DEFAULT_SERVER = "*Auto Detect" DEFAULT_SERVER: Final = "*Auto Detect"
ATTRIBUTION = "Data retrieved from Speedtest.net by Ookla" ATTRIBUTION: Final = "Data retrieved from Speedtest.net by Ookla"
ICON = "mdi:speedometer" ICON: Final = "mdi:speedometer"
PLATFORMS: Final = ["sensor"]

View file

@ -54,26 +54,28 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
self._attr_name = f"{DEFAULT_NAME} {SENSOR_TYPES[sensor_type][0]}" self._attr_name = f"{DEFAULT_NAME} {SENSOR_TYPES[sensor_type][0]}"
self._attr_unit_of_measurement = SENSOR_TYPES[self.type][1] self._attr_unit_of_measurement = SENSOR_TYPES[self.type][1]
self._attr_unique_id = sensor_type self._attr_unique_id = sensor_type
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
@property @property
def extra_state_attributes(self) -> dict[str, Any] | None: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if not self.coordinator.data: if self.coordinator.data:
return None self._attrs.update(
{
ATTR_SERVER_NAME: self.coordinator.data["server"]["name"],
ATTR_SERVER_COUNTRY: self.coordinator.data["server"]["country"],
ATTR_SERVER_ID: self.coordinator.data["server"]["id"],
}
)
attributes = { if self.type == "download":
ATTR_ATTRIBUTION: ATTRIBUTION, self._attrs[ATTR_BYTES_RECEIVED] = self.coordinator.data[
ATTR_SERVER_NAME: self.coordinator.data["server"]["name"], "bytes_received"
ATTR_SERVER_COUNTRY: self.coordinator.data["server"]["country"], ]
ATTR_SERVER_ID: self.coordinator.data["server"]["id"], elif self.type == "upload":
} self._attrs[ATTR_BYTES_SENT] = self.coordinator.data["bytes_sent"]
if self.type == "download": return self._attrs
attributes[ATTR_BYTES_RECEIVED] = self.coordinator.data["bytes_received"]
elif self.type == "upload":
attributes[ATTR_BYTES_SENT] = self.coordinator.data["bytes_sent"]
return attributes
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""
@ -91,14 +93,12 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
self.async_on_remove(self.coordinator.async_add_listener(update)) self.async_on_remove(self.coordinator.async_add_listener(update))
self._update_state() self._update_state()
def _update_state(self) -> None: def _update_state(self):
"""Update sensors state.""" """Update sensors state."""
if not self.coordinator.data: if self.coordinator.data:
return if self.type == "ping":
self._attr_state = self.coordinator.data["ping"]
if self.type == "ping": elif self.type == "download":
self._attr_state = self.coordinator.data["ping"] self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2)
elif self.type == "download": elif self.type == "upload":
self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2) self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2)
elif self.type == "upload":
self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2)

View file

@ -6,8 +6,7 @@
} }
}, },
"abort": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
"wrong_server_id": "Server ID is not valid"
} }
}, },
"options": { "options": {
@ -21,4 +20,4 @@
} }
} }
} }
} }

View file

@ -1,8 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"single_instance_allowed": "Already configured. Only a single configuration possible.", "single_instance_allowed": "Already configured. Only a single configuration possible."
"wrong_server_id": "Server ID is not valid"
}, },
"step": { "step": {
"user": { "user": {

View file

@ -0,0 +1,16 @@
"""Conftest for speedtestdotnet."""
from unittest.mock import patch
import pytest
from tests.components.speedtestdotnet import MOCK_RESULTS, MOCK_SERVERS
@pytest.fixture(autouse=True)
def mock_api():
"""Mock entry setup."""
with patch("speedtest.Speedtest") as mock_api:
mock_api.return_value.get_servers.return_value = MOCK_SERVERS
mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0]
mock_api.return_value.results.dict.return_value = MOCK_RESULTS
yield mock_api

View file

@ -1,8 +1,7 @@
"""Tests for SpeedTest config flow.""" """Tests for SpeedTest config flow."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import MagicMock
import pytest
from speedtest import NoMatchedServers from speedtest import NoMatchedServers
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
@ -15,23 +14,12 @@ from homeassistant.components.speedtestdotnet.const import (
SENSOR_TYPES, SENSOR_TYPES,
) )
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from . import MOCK_SERVERS
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@pytest.fixture(name="mock_setup") async def test_flow_works(hass: HomeAssistant) -> None:
def mock_setup():
"""Mock entry setup."""
with patch(
"homeassistant.components.speedtestdotnet.async_setup_entry",
return_value=True,
):
yield
async def test_flow_works(hass, mock_setup):
"""Test user config.""" """Test user config."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -43,92 +31,104 @@ async def test_flow_works(hass, mock_setup):
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "SpeedTest"
async def test_import_fails(hass, mock_setup): async def test_import_fails(hass: HomeAssistant, mock_api: MagicMock) -> None:
"""Test import step fails if server_id is not valid.""" """Test import step fails if server_id is not valid."""
with patch("speedtest.Speedtest") as mock_api: mock_api.return_value.get_servers.side_effect = NoMatchedServers
mock_api.return_value.get_servers.side_effect = NoMatchedServers result = await hass.config_entries.flow.async_init(
result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN,
speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_IMPORT},
context={"source": config_entries.SOURCE_IMPORT}, data={
data={ CONF_SERVER_ID: "223",
CONF_SERVER_ID: "223", CONF_MANUAL: True,
CONF_MANUAL: True, CONF_SCAN_INTERVAL: timedelta(minutes=1),
CONF_SCAN_INTERVAL: timedelta(minutes=1), CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES),
CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES), },
}, )
) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "wrong_server_id"
assert result["reason"] == "wrong_server_id"
async def test_import_success(hass, mock_setup): async def test_import_success(hass):
"""Test import step is successful if server_id is valid.""" """Test import step is successful if server_id is valid."""
with patch("speedtest.Speedtest"): result = await hass.config_entries.flow.async_init(
result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN,
speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_IMPORT},
context={"source": config_entries.SOURCE_IMPORT}, data={
data={ CONF_SERVER_ID: "1",
CONF_SERVER_ID: "1", CONF_MANUAL: True,
CONF_MANUAL: True, CONF_SCAN_INTERVAL: timedelta(minutes=1),
CONF_SCAN_INTERVAL: timedelta(minutes=1), CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES),
CONF_MONITORED_CONDITIONS: list(SENSOR_TYPES), },
}, )
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "SpeedTest" assert result["title"] == "SpeedTest"
assert result["data"][CONF_SERVER_ID] == "1" assert result["data"][CONF_SERVER_ID] == "1"
assert result["data"][CONF_MANUAL] is True assert result["data"][CONF_MANUAL] is True
assert result["data"][CONF_SCAN_INTERVAL] == 1 assert result["data"][CONF_SCAN_INTERVAL] == 1
async def test_options(hass): async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None:
"""Test updating options.""" """Test updating options."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="SpeedTest", title="SpeedTest",
data={},
options={},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("speedtest.Speedtest") as mock_api: await hass.config_entries.async_setup(entry.entry_id)
mock_api.return_value.get_servers.return_value = MOCK_SERVERS await hass.async_block_till_done()
await hass.config_entries.async_setup(entry.entry_id)
result = await hass.config_entries.options.async_init(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init" assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SCAN_INTERVAL: 30, CONF_SCAN_INTERVAL: 30,
CONF_MANUAL: False, CONF_MANUAL: True,
}, },
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"] == { assert result["data"] == {
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SERVER_ID: "1",
CONF_SCAN_INTERVAL: 30,
CONF_MANUAL: True,
}
await hass.async_block_till_done()
assert hass.data[DOMAIN].update_interval is None
# test setting the option to update periodically
result2 = await hass.config_entries.options.async_init(entry.entry_id)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "init"
result2 = await hass.config_entries.options.async_configure(
result2["flow_id"],
user_input={
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SERVER_ID: "1",
CONF_SCAN_INTERVAL: 30, CONF_SCAN_INTERVAL: 30,
CONF_MANUAL: False, CONF_MANUAL: False,
} },
)
await hass.async_block_till_done()
assert hass.data[DOMAIN].update_interval == timedelta(minutes=30)
async def test_integration_already_configured(hass): async def test_integration_already_configured(hass: HomeAssistant) -> None:
"""Test integration is already configured.""" """Test integration is already configured."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={},
options={},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View file

@ -1,79 +1,113 @@
"""Tests for SpeedTest integration.""" """Tests for SpeedTest integration."""
from unittest.mock import patch from unittest.mock import MagicMock
import speedtest import speedtest
from homeassistant import config_entries from homeassistant.components.speedtestdotnet.const import (
from homeassistant.components import speedtestdotnet CONF_MANUAL,
from homeassistant.setup import async_setup_component CONF_SERVER_ID,
CONF_SERVER_NAME,
DOMAIN,
SPEED_TEST_SERVICE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_SCAN_INTERVAL, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_setup_with_config(hass): async def test_successful_config_entry(hass: HomeAssistant) -> None:
"""Test that we import the config and setup the integration."""
config = {
speedtestdotnet.DOMAIN: {
speedtestdotnet.CONF_SERVER_ID: "1",
speedtestdotnet.CONF_MANUAL: True,
speedtestdotnet.CONF_SCAN_INTERVAL: "00:01:00",
}
}
with patch("speedtest.Speedtest"):
assert await async_setup_component(hass, speedtestdotnet.DOMAIN, config)
async def test_successful_config_entry(hass):
"""Test that SpeedTestDotNet is configured successfully.""" """Test that SpeedTestDotNet is configured successfully."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=speedtestdotnet.DOMAIN, domain=DOMAIN,
data={}, data={},
options={
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SERVER_ID: "1",
CONF_SCAN_INTERVAL: 30,
CONF_MANUAL: False,
},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("speedtest.Speedtest"), patch( await hass.config_entries.async_setup(entry.entry_id)
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
return_value=True,
) as forward_entry_setup:
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED
assert forward_entry_setup.mock_calls[0][1] == ( assert hass.data[DOMAIN]
entry, assert hass.services.has_service(DOMAIN, SPEED_TEST_SERVICE)
"sensor",
)
async def test_setup_failed(hass): async def test_setup_failed(hass: HomeAssistant, mock_api: MagicMock) -> None:
"""Test SpeedTestDotNet failed due to an error.""" """Test SpeedTestDotNet failed due to an error."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=speedtestdotnet.DOMAIN, domain=DOMAIN,
data={},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("speedtest.Speedtest", side_effect=speedtest.ConfigRetrievalError): mock_api.side_effect = speedtest.ConfigRetrievalError
await hass.config_entries.async_setup(entry.entry_id)
await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
async def test_unload_entry(hass): async def test_unload_entry(hass: HomeAssistant) -> None:
"""Test removing SpeedTestDotNet.""" """Test removing SpeedTestDotNet."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=speedtestdotnet.DOMAIN, domain=DOMAIN,
data={},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("speedtest.Speedtest"): await hass.config_entries.async_setup(entry.entry_id)
await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done()
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert entry.state is ConfigEntryState.NOT_LOADED
assert speedtestdotnet.DOMAIN not in hass.data assert DOMAIN not in hass.data
async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> None:
"""Test configured server id is not found."""
entry = MockConfigEntry(
domain=DOMAIN,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.data[DOMAIN]
mock_api.return_value.get_servers.side_effect = speedtest.NoMatchedServers
await hass.data[DOMAIN].async_refresh()
await hass.async_block_till_done()
state = hass.states.get("sensor.speedtest_ping")
assert state is not None
assert state.state == STATE_UNAVAILABLE
async def test_get_best_server_error(hass: HomeAssistant, mock_api: MagicMock) -> None:
"""Test configured server id is not found."""
entry = MockConfigEntry(
domain=DOMAIN,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.data[DOMAIN]
mock_api.return_value.get_best_server.side_effect = (
speedtest.SpeedtestBestServerFailure(
"Unable to connect to servers to test latency."
)
)
await hass.data[DOMAIN].async_refresh()
await hass.async_block_till_done()
state = hass.states.get("sensor.speedtest_ping")
assert state is not None
assert state.state == STATE_UNAVAILABLE

View file

@ -1,26 +1,28 @@
"""Tests for SpeedTest sensors.""" """Tests for SpeedTest sensors."""
from unittest.mock import patch from unittest.mock import MagicMock
from homeassistant.components import speedtestdotnet from homeassistant.components import speedtestdotnet
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME, SENSOR_TYPES from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME, SENSOR_TYPES
from homeassistant.core import HomeAssistant
from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_speedtestdotnet_sensors(hass): async def test_speedtestdotnet_sensors(
hass: HomeAssistant, mock_api: MagicMock
) -> None:
"""Test sensors created for speedtestdotnet integration.""" """Test sensors created for speedtestdotnet integration."""
entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={}) entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={})
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("speedtest.Speedtest") as mock_api: mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0]
mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0] mock_api.return_value.results.dict.return_value = MOCK_RESULTS
mock_api.return_value.results.dict.return_value = MOCK_RESULTS
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
@ -28,4 +30,5 @@ async def test_speedtestdotnet_sensors(hass):
sensor = hass.states.get( sensor = hass.states.get(
f"sensor.{DEFAULT_NAME}_{SENSOR_TYPES[sensor_type][0]}" f"sensor.{DEFAULT_NAME}_{SENSOR_TYPES[sensor_type][0]}"
) )
assert sensor
assert sensor.state == MOCK_STATES[sensor_type] assert sensor.state == MOCK_STATES[sensor_type]