Add config flow to pyLoad integration (#120135)
* Add config flow to pyLoad integration * address issues * remove suggested values * Fix exception * abort import flow on error * readd repair issues on error * fix ruff * changes * changes * exception hints
This commit is contained in:
parent
f257fcb0d1
commit
28fb361c64
13 changed files with 1041 additions and 112 deletions
|
@ -1 +1,70 @@
|
|||
"""The pyload component."""
|
||||
"""The pyLoad integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from pyloadapi.api import PyLoadAPI
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type PyLoadConfigEntry = ConfigEntry[PyLoadAPI]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool:
|
||||
"""Set up pyLoad from a config entry."""
|
||||
|
||||
url = (
|
||||
f"{"https" if entry.data[CONF_SSL] else "http"}://"
|
||||
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/"
|
||||
)
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass,
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
cookie_jar=CookieJar(unsafe=True),
|
||||
)
|
||||
pyloadapi = PyLoadAPI(
|
||||
session,
|
||||
api_url=url,
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await pyloadapi.login()
|
||||
except CannotConnect as e:
|
||||
raise ConfigEntryNotReady(
|
||||
"Unable to connect and retrieve data from pyLoad API"
|
||||
) from e
|
||||
except ParserError as e:
|
||||
raise ConfigEntryNotReady("Unable to parse data from pyLoad API") from e
|
||||
except InvalidAuth as e:
|
||||
raise ConfigEntryError(
|
||||
f"Authentication failed for {entry.data[CONF_USERNAME]}, check your login credentials"
|
||||
) from e
|
||||
|
||||
entry.runtime_data = pyloadapi
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
|
120
homeassistant/components/pyload/config_flow.py
Normal file
120
homeassistant/components/pyload/config_flow.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
"""Config flow for pyLoad integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from pyloadapi.api import PyLoadAPI
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Required(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None:
|
||||
"""Validate the user input and try to connect to PyLoad."""
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass,
|
||||
user_input[CONF_VERIFY_SSL],
|
||||
cookie_jar=CookieJar(unsafe=True),
|
||||
)
|
||||
|
||||
url = (
|
||||
f"{"https" if user_input[CONF_SSL] else "http"}://"
|
||||
f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/"
|
||||
)
|
||||
pyload = PyLoadAPI(
|
||||
session,
|
||||
api_url=url,
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
await pyload.login()
|
||||
|
||||
|
||||
class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for pyLoad."""
|
||||
|
||||
VERSION = 1
|
||||
# store values from yaml import so we can use them as
|
||||
# suggested values when the configuration step is resumed
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except (CannotConnect, ParserError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
title = user_input.pop(CONF_NAME, DEFAULT_NAME)
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import config from yaml."""
|
||||
|
||||
config = {
|
||||
CONF_NAME: import_info.get(CONF_NAME),
|
||||
CONF_HOST: import_info.get(CONF_HOST, DEFAULT_HOST),
|
||||
CONF_PASSWORD: import_info.get(CONF_PASSWORD, ""),
|
||||
CONF_PORT: import_info.get(CONF_PORT, DEFAULT_PORT),
|
||||
CONF_SSL: import_info.get(CONF_SSL, False),
|
||||
CONF_USERNAME: import_info.get(CONF_USERNAME, ""),
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
result = await self.async_step_user(config)
|
||||
|
||||
if errors := result.get("errors"):
|
||||
return self.async_abort(reason=errors["base"])
|
||||
return result
|
|
@ -5,3 +5,5 @@ DOMAIN = "pyload"
|
|||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "pyLoad"
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=pyload"}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"domain": "pyload",
|
||||
"name": "pyLoad",
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/pyload",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
|
|
|
@ -7,7 +7,6 @@ from enum import StrEnum
|
|||
import logging
|
||||
from time import monotonic
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from pyloadapi import (
|
||||
CannotConnect,
|
||||
InvalidAuth,
|
||||
|
@ -23,6 +22,7 @@ from homeassistant.components.sensor import (
|
|||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MONITORED_VARIABLES,
|
||||
|
@ -33,14 +33,15 @@ from homeassistant.const import (
|
|||
CONF_USERNAME,
|
||||
UnitOfDataRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
|
||||
from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT
|
||||
from . import PyLoadConfigEntry
|
||||
from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, ISSUE_PLACEHOLDER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -82,41 +83,63 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the pyLoad sensors."""
|
||||
host = config[CONF_HOST]
|
||||
port = config[CONF_PORT]
|
||||
protocol = "https" if config[CONF_SSL] else "http"
|
||||
name = config[CONF_NAME]
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
url = f"{protocol}://{host}:{port}/"
|
||||
"""Import config from yaml."""
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass,
|
||||
verify_ssl=False,
|
||||
cookie_jar=CookieJar(unsafe=True),
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
pyloadapi = PyLoadAPI(session, api_url=url, username=username, password=password)
|
||||
try:
|
||||
await pyloadapi.login()
|
||||
except CannotConnect as conn_err:
|
||||
raise PlatformNotReady(
|
||||
"Unable to connect and retrieve data from pyLoad API"
|
||||
) from conn_err
|
||||
except ParserError as e:
|
||||
raise PlatformNotReady("Unable to parse data from pyLoad API") from e
|
||||
except InvalidAuth as e:
|
||||
raise PlatformNotReady(
|
||||
f"Authentication failed for {config[CONF_USERNAME]}, check your login credentials"
|
||||
) from e
|
||||
_LOGGER.debug(result)
|
||||
if (
|
||||
result.get("type") == FlowResultType.CREATE_ENTRY
|
||||
or result.get("reason") == "already_configured"
|
||||
):
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
breaks_in_ha_version="2025.2.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "pyLoad",
|
||||
},
|
||||
)
|
||||
elif error := result.get("reason"):
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{error}",
|
||||
breaks_in_ha_version="2025.2.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{error}",
|
||||
translation_placeholders=ISSUE_PLACEHOLDER,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: PyLoadConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the pyLoad sensors."""
|
||||
|
||||
pyloadapi = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
PyLoadSensor(
|
||||
api=pyloadapi, entity_description=description, client_name=name
|
||||
api=pyloadapi,
|
||||
entity_description=description,
|
||||
client_name=entry.title,
|
||||
entry_id=entry.entry_id,
|
||||
)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
),
|
||||
|
@ -128,12 +151,17 @@ class PyLoadSensor(SensorEntity):
|
|||
"""Representation of a pyLoad sensor."""
|
||||
|
||||
def __init__(
|
||||
self, api: PyLoadAPI, entity_description: SensorEntityDescription, client_name
|
||||
self,
|
||||
api: PyLoadAPI,
|
||||
entity_description: SensorEntityDescription,
|
||||
client_name: str,
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initialize a new pyLoad sensor."""
|
||||
self._attr_name = f"{client_name} {entity_description.name}"
|
||||
self.type = entity_description.key
|
||||
self.api = api
|
||||
self._attr_unique_id = f"{entry_id}_{entity_description.key}"
|
||||
self.entity_description = entity_description
|
||||
self._attr_available = False
|
||||
self.data: StatusServerResponse
|
||||
|
|
44
homeassistant/components/pyload/strings.json
Normal file
44
homeassistant/components/pyload/strings.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "The name to use for your pyLoad instance in Home Assistant",
|
||||
"host": "The hostname or IP address of the device running your pyLoad instance.",
|
||||
"port": "pyLoad uses port 8000 by default."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The pyLoad YAML configuration import failed",
|
||||
"description": "Configuring pyLoad using YAML is being removed but there was a connection error when trying to import the YAML configuration.\n\nPlease verify that you have a stable internet connection and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_auth": {
|
||||
"title": "The pyLoad YAML configuration import failed",
|
||||
"description": "Configuring pyLoad using YAML is being removed but there was an authentication error when trying to import the YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"title": "The pyLoad YAML configuration import failed",
|
||||
"description": "Configuring pyLoad using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the pyLoad YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -435,6 +435,7 @@ FLOWS = {
|
|||
"pushover",
|
||||
"pvoutput",
|
||||
"pvpc_hourly_pricing",
|
||||
"pyload",
|
||||
"qbittorrent",
|
||||
"qingping",
|
||||
"qnap",
|
||||
|
|
|
@ -4781,7 +4781,7 @@
|
|||
"pyload": {
|
||||
"name": "pyLoad",
|
||||
"integration_type": "service",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"python_script": {
|
||||
|
|
|
@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch
|
|||
from pyloadapi.types import LoginResponse, StatusServerResponse
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MONITORED_VARIABLES,
|
||||
|
@ -15,25 +16,46 @@ from homeassistant.const import (
|
|||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
USER_INPUT = {
|
||||
CONF_HOST: "pyload.local",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_PORT: 8000,
|
||||
CONF_SSL: True,
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
YAML_INPUT = {
|
||||
CONF_HOST: "pyload.local",
|
||||
CONF_MONITORED_VARIABLES: ["speed"],
|
||||
CONF_NAME: "test-name",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_PLATFORM: "pyload",
|
||||
CONF_PORT: 8000,
|
||||
CONF_SSL: True,
|
||||
CONF_USERNAME: "test-username",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.pyload.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyload_config() -> ConfigType:
|
||||
"""Mock pyload configuration entry."""
|
||||
return {
|
||||
"sensor": {
|
||||
CONF_PLATFORM: "pyload",
|
||||
CONF_HOST: "localhost",
|
||||
CONF_PORT: 8000,
|
||||
CONF_USERNAME: "username",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_SSL: True,
|
||||
CONF_MONITORED_VARIABLES: ["speed"],
|
||||
CONF_NAME: "pyload",
|
||||
}
|
||||
}
|
||||
return {"sensor": YAML_INPUT}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -41,12 +63,15 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]:
|
|||
"""Mock PyLoadAPI."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.pyload.sensor.PyLoadAPI",
|
||||
autospec=True,
|
||||
"homeassistant.components.pyload.PyLoadAPI", autospec=True
|
||||
) as mock_client,
|
||||
patch("homeassistant.components.pyload.config_flow.PyLoadAPI", new=mock_client),
|
||||
patch("homeassistant.components.pyload.sensor.PyLoadAPI", new=mock_client),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.username = "username"
|
||||
client.api_url = "https://pyload.local:8000/"
|
||||
|
||||
client.login.return_value = LoginResponse(
|
||||
{
|
||||
"_permanent": True,
|
||||
|
@ -75,3 +100,11 @@ def mock_pyloadapi() -> Generator[AsyncMock, None, None]:
|
|||
|
||||
client.free_space.return_value = 99999999999
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock pyLoad configuration entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN, title=DEFAULT_NAME, data=USER_INPUT, entry_id="XXXXXXXXXXXXXX"
|
||||
)
|
||||
|
|
|
@ -1,4 +1,328 @@
|
|||
# serializer version: 1
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'pyLoad Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[CannotConnect][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'pyLoad Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[InvalidAuth][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'pyLoad Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_update_exceptions[ParserError][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors_unavailable[CannotConnect][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors_unavailable[InvalidAuth][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors_unavailable[ParserError][sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -14,3 +338,57 @@
|
|||
'state': '5.405963',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.pyload_speed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'pyLoad Speed',
|
||||
'platform': 'pyload',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'XXXXXXXXXXXXXX_speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.pyload_speed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'data_rate',
|
||||
'friendly_name': 'pyLoad Speed',
|
||||
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pyload_speed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.405963',
|
||||
})
|
||||
# ---
|
||||
|
|
166
tests/components/pyload/test_config_flow.py
Normal file
166
tests/components/pyload/test_config_flow.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
"""Test the pyLoad config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .conftest import USER_INPUT, YAML_INPUT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"] == USER_INPUT
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
(InvalidAuth, "invalid_auth"),
|
||||
(CannotConnect, "cannot_connect"),
|
||||
(ParserError, "cannot_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
mock_pyloadapi.login.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
mock_pyloadapi.login.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"] == USER_INPUT
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_flow_user_already_configured(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock
|
||||
) -> None:
|
||||
"""Test we abort user data set when entry is already configured."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_flow_import(
|
||||
hass: HomeAssistant,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that we can import a YAML config."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=YAML_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-name"
|
||||
assert result["data"] == USER_INPUT
|
||||
|
||||
|
||||
async def test_flow_import_already_configured(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock
|
||||
) -> None:
|
||||
"""Test we abort import data set when entry is already configured."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=YAML_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "reason"),
|
||||
[
|
||||
(InvalidAuth, "invalid_auth"),
|
||||
(CannotConnect, "cannot_connect"),
|
||||
(ParserError, "cannot_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_flow_import_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
exception: Exception,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Test we abort import data set when entry is already configured."""
|
||||
|
||||
mock_pyloadapi.login.side_effect = exception
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=YAML_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == reason
|
63
tests/components/pyload/test_init.py
Normal file
63
tests/components/pyload/test_init.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
"""Test pyLoad init."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_entry_setup_unload(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: MagicMock,
|
||||
) -> None:
|
||||
"""Test integration setup and unload."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect"),
|
||||
[CannotConnect, ParserError],
|
||||
)
|
||||
async def test_config_entry_setup_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: MagicMock,
|
||||
side_effect: Exception,
|
||||
) -> None:
|
||||
"""Test config entry not ready."""
|
||||
mock_pyloadapi.login.side_effect = side_effect
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_setup_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: MagicMock,
|
||||
) -> None:
|
||||
"""Test config entry authentication."""
|
||||
mock_pyloadapi.login.side_effect = InvalidAuth
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
|
@ -7,108 +7,74 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
|||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.pyload.const import DOMAIN
|
||||
from homeassistant.components.pyload.sensor import SCAN_INTERVAL
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
SENSORS = ["sensor.pyload_speed"]
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pyloadapi")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
pyload_config: ConfigType,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setup of the pyload sensor platform."""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, pyload_config)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for sensor in SENSORS:
|
||||
result = hass.states.get(sensor)
|
||||
assert result == snapshot
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_exception"),
|
||||
[
|
||||
(CannotConnect, "Unable to connect and retrieve data from pyLoad API"),
|
||||
(ParserError, "Unable to parse data from pyLoad API"),
|
||||
(
|
||||
InvalidAuth,
|
||||
"Authentication failed for username, check your login credentials",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_setup_exceptions(
|
||||
hass: HomeAssistant,
|
||||
pyload_config: ConfigType,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_exception: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test exceptions during setup up pyLoad platform."""
|
||||
|
||||
mock_pyloadapi.login.side_effect = exception
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, pyload_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all(DOMAIN)) == 0
|
||||
assert expected_exception in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_exception"),
|
||||
[
|
||||
(CannotConnect, "Unable to connect and retrieve data from pyLoad API"),
|
||||
(ParserError, "Unable to parse data from pyLoad API"),
|
||||
(InvalidAuth, "Authentication failed, trying to reauthenticate"),
|
||||
],
|
||||
"exception",
|
||||
[CannotConnect, InvalidAuth, ParserError],
|
||||
)
|
||||
async def test_sensor_update_exceptions(
|
||||
hass: HomeAssistant,
|
||||
pyload_config: ConfigType,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_exception: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test exceptions during update of pyLoad sensor."""
|
||||
"""Test if pyLoad sensors go unavailable when exceptions occur (except ParserErrors)."""
|
||||
|
||||
mock_pyloadapi.get_status.side_effect = exception
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, pyload_config)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all(DOMAIN)) == 1
|
||||
assert expected_exception in caplog.text
|
||||
mock_pyloadapi.get_status.side_effect = exception
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for sensor in SENSORS:
|
||||
assert hass.states.get(sensor).state == STATE_UNAVAILABLE
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_sensor_invalid_auth(
|
||||
hass: HomeAssistant,
|
||||
pyload_config: ConfigType,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test invalid auth during sensor update."""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, pyload_config)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all(DOMAIN)) == 1
|
||||
|
||||
mock_pyloadapi.get_status.side_effect = InvalidAuth
|
||||
mock_pyloadapi.login.side_effect = InvalidAuth
|
||||
|
@ -121,3 +87,61 @@ async def test_sensor_invalid_auth(
|
|||
"Authentication failed for username, check your login credentials"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_platform_setup_triggers_import_flow(
|
||||
hass: HomeAssistant,
|
||||
pyload_config: ConfigType,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
) -> None:
|
||||
"""Test if an issue is created when attempting setup from yaml config."""
|
||||
|
||||
assert await async_setup_component(hass, SENSOR_DOMAIN, pyload_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "reason"),
|
||||
[
|
||||
(InvalidAuth, "invalid_auth"),
|
||||
(CannotConnect, "cannot_connect"),
|
||||
(ParserError, "cannot_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_deprecated_yaml_import_issue(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
pyload_config: ConfigType,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
exception: Exception,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Test an issue is created when attempting setup from yaml config and an error happens."""
|
||||
|
||||
mock_pyloadapi.login.side_effect = exception
|
||||
await async_setup_component(hass, SENSOR_DOMAIN, pyload_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN, issue_id=f"deprecated_yaml_import_issue_{reason}"
|
||||
)
|
||||
|
||||
|
||||
async def test_deprecated_yaml(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
pyload_config: ConfigType,
|
||||
mock_pyloadapi: AsyncMock,
|
||||
) -> None:
|
||||
"""Test an issue is created when we import from yaml config."""
|
||||
|
||||
await async_setup_component(hass, SENSOR_DOMAIN, pyload_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}"
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue