diff --git a/.coveragerc b/.coveragerc index fa6ae5ba0d220..a533343bf060d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -395,7 +395,8 @@ omit = homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/sensor.py - homeassistant/components/frontier_silicon/const.py + homeassistant/components/frontier_silicon/__init__.py + homeassistant/components/frontier_silicon/browse_media.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 9020d229fe903..f95d89fec4767 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -401,6 +401,7 @@ build.json @home-assistant/supervisor /homeassistant/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend /homeassistant/components/frontier_silicon/ @wlcrs +/tests/components/frontier_silicon/ @wlcrs /homeassistant/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood /homeassistant/components/garages_amsterdam/ @klaasnicolaas diff --git a/homeassistant/components/frontier_silicon/__init__.py b/homeassistant/components/frontier_silicon/__init__.py index ddd74ca8efe58..4a884063f83c7 100644 --- a/homeassistant/components/frontier_silicon/__init__.py +++ b/homeassistant/components/frontier_silicon/__init__.py @@ -1 +1,45 @@ -"""The frontier_silicon component.""" +"""The Frontier Silicon integration.""" +from __future__ import annotations + +import logging + +from afsapi import AFSAPI, ConnectionError as FSConnectionError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady + +from .const import CONF_PIN, CONF_WEBFSAPI_URL, DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Frontier Silicon from a config entry.""" + + webfsapi_url = entry.data[CONF_WEBFSAPI_URL] + pin = entry.data[CONF_PIN] + + afsapi = AFSAPI(webfsapi_url, pin) + + try: + await afsapi.get_power() + except FSConnectionError as exception: + raise PlatformNotReady from exception + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = afsapi + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + 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/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py new file mode 100644 index 0000000000000..5e9472de62e28 --- /dev/null +++ b/homeassistant/components/frontier_silicon/config_flow.py @@ -0,0 +1,178 @@ +"""Config flow for Frontier Silicon Media Player integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from afsapi import AFSAPI, ConnectionError as FSConnectionError, InvalidPinException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_PIN, CONF_WEBFSAPI_URL, DEFAULT_PIN, 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): int, + } +) + +STEP_DEVICE_CONFIG_DATA_SCHEMA = vol.Schema( + { + vol.Required( + CONF_PIN, + default=DEFAULT_PIN, + ): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Frontier Silicon Media Player.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize flow.""" + + self._webfsapi_url: str | None = None + self._name: str | None = None + self._unique_id: str | None = None + + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + """Handle the import of legacy configuration.yaml entries.""" + + device_url = f"http://{import_info[CONF_HOST]}:{import_info[CONF_PORT]}/device" + try: + self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url) + except FSConnectionError: + return self.async_abort(reason="cannot_connect") + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + return self.async_abort(reason="unknown") + + try: + afsapi = AFSAPI(self._webfsapi_url, import_info[CONF_PIN]) + + self._unique_id = await afsapi.get_radio_id() + except FSConnectionError: + return self.async_abort(reason="cannot_connect") + except InvalidPinException: + return self.async_abort(reason="invalid_auth") + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + return self.async_abort(reason="unknown") + + await self.async_set_unique_id(self._unique_id, raise_on_progress=False) + self._abort_if_unique_id_configured() + + self._name = import_info[CONF_NAME] or "Radio" + + return await self._create_entry(pin=import_info[CONF_PIN]) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step of manual configuration.""" + errors = {} + + if user_input: + device_url = ( + f"http://{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/device" + ) + try: + self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url) + except FSConnectionError: + errors["base"] = "cannot_connect" + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + errors["base"] = "unknown" + else: + return await self._async_step_device_config_if_needed() + + data_schema = self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def _async_step_device_config_if_needed(self) -> FlowResult: + """Most users will not have changed the default PIN on their radio. + + We try to use this default PIN, and only if this fails ask for it via `async_step_device_config` + """ + + try: + # try to login with default pin + afsapi = AFSAPI(self._webfsapi_url, DEFAULT_PIN) + + self._name = await afsapi.get_friendly_name() + except InvalidPinException: + # Ask for a PIN + return await self.async_step_device_config() + + self.context["title_placeholders"] = {"name": self._name} + + self._unique_id = await afsapi.get_radio_id() + await self.async_set_unique_id(self._unique_id) + self._abort_if_unique_id_configured() + + return await self._create_entry() + + async def async_step_device_config( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle device configuration step. + + We ask for the PIN in this step. + """ + assert self._webfsapi_url is not None + + if user_input is None: + return self.async_show_form( + step_id="device_config", data_schema=STEP_DEVICE_CONFIG_DATA_SCHEMA + ) + + errors = {} + + try: + afsapi = AFSAPI(self._webfsapi_url, user_input[CONF_PIN]) + + self._name = await afsapi.get_friendly_name() + + except FSConnectionError: + errors["base"] = "cannot_connect" + except InvalidPinException: + errors["base"] = "invalid_auth" + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) + errors["base"] = "unknown" + else: + self._unique_id = await afsapi.get_radio_id() + await self.async_set_unique_id(self._unique_id) + self._abort_if_unique_id_configured() + return await self._create_entry(pin=user_input[CONF_PIN]) + + data_schema = self.add_suggested_values_to_schema( + STEP_DEVICE_CONFIG_DATA_SCHEMA, user_input + ) + return self.async_show_form( + step_id="device_config", + data_schema=data_schema, + errors=errors, + ) + + async def _create_entry(self, pin: str | None = None) -> FlowResult: + """Create the entry.""" + assert self._name is not None + assert self._webfsapi_url is not None + + data = {CONF_WEBFSAPI_URL: self._webfsapi_url, CONF_PIN: pin or DEFAULT_PIN} + + return self.async_create_entry(title=self._name, data=data) diff --git a/homeassistant/components/frontier_silicon/const.py b/homeassistant/components/frontier_silicon/const.py index 9ee17c0320e1a..9206db89166b2 100644 --- a/homeassistant/components/frontier_silicon/const.py +++ b/homeassistant/components/frontier_silicon/const.py @@ -1,6 +1,9 @@ """Constants for the Frontier Silicon Media Player integration.""" DOMAIN = "frontier_silicon" +CONF_WEBFSAPI_URL = "webfsapi_url" +CONF_PIN = "pin" + DEFAULT_PIN = "1234" DEFAULT_PORT = 80 diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 322c1b90b2647..62e7e61703456 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,6 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "codeowners": ["@wlcrs"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "iot_class": "local_polling", "requirements": ["afsapi==0.2.7"] diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 0e3eb168484a4..b05ba272a19d9 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -21,15 +21,17 @@ MediaPlayerState, MediaType, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .browse_media import browse_node, browse_top_level -from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET +from .const import CONF_PIN, DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET _LOGGER = logging.getLogger(__name__) @@ -49,7 +51,11 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Frontier Silicon platform.""" + """Set up the Frontier Silicon platform. + + YAML is deprecated, and imported automatically. + SSDP discovery is temporarily retained - to be refactor subsequently. + """ if discovery_info is not None: webfsapi_url = await AFSAPI.get_webfsapi_endpoint( discovery_info["ssdp_description"] @@ -61,24 +67,41 @@ async def async_setup_platform( [AFSAPIDevice(name, afsapi)], True, ) + return - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - password = config.get(CONF_PASSWORD) - name = config.get(CONF_NAME) + ir.async_create_issue( + hass, + DOMAIN, + "remove_yaml", + breaks_in_ha_version="2023.6.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="removed_yaml", + ) - try: - webfsapi_url = await AFSAPI.get_webfsapi_endpoint( - f"http://{host}:{port}/device" - ) - except FSConnectionError: - _LOGGER.error( - "Could not add the FSAPI device at %s:%s -> %s", host, port, password - ) - return - afsapi = AFSAPI(webfsapi_url, password) - async_add_entities([AFSAPIDevice(name, afsapi)], True) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_NAME: config.get(CONF_NAME), + CONF_HOST: config.get(CONF_HOST), + CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), + CONF_PIN: config.get(CONF_PASSWORD, DEFAULT_PIN), + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Frontier Silicon entity.""" + + afsapi: AFSAPI = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities([AFSAPIDevice(config_entry.title, afsapi)], True) class AFSAPIDevice(MediaPlayerEntity): diff --git a/homeassistant/components/frontier_silicon/strings.json b/homeassistant/components/frontier_silicon/strings.json new file mode 100644 index 0000000000000..85b0b6958afc4 --- /dev/null +++ b/homeassistant/components/frontier_silicon/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "title": "Frontier Silicon Setup", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + }, + "device_config": { + "title": "Device Configuration", + "description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + } + }, + "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": { + "removed_yaml": { + "title": "The Frontier Silicon YAML configuration has been removed", + "description": "Configuring Frontier Silicon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8e13dd971e5cc..6656972f8b07c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -145,6 +145,7 @@ "fritzbox", "fritzbox_callmonitor", "fronius", + "frontier_silicon", "fully_kiosk", "garages_amsterdam", "gdacs", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e8350284f13e5..9742af1edfca5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1818,7 +1818,7 @@ "frontier_silicon": { "name": "Frontier Silicon", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "fully_kiosk": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a241892ff52cb..1dfb0466be641 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,6 +78,9 @@ adguardhome==0.6.1 # homeassistant.components.advantage_air advantage_air==0.4.1 +# homeassistant.components.frontier_silicon +afsapi==0.2.7 + # homeassistant.components.agent_dvr agent-py==0.0.23 diff --git a/tests/components/frontier_silicon/__init__.py b/tests/components/frontier_silicon/__init__.py new file mode 100644 index 0000000000000..6a039dc29acd3 --- /dev/null +++ b/tests/components/frontier_silicon/__init__.py @@ -0,0 +1 @@ +"""Tests for the Frontier Silicon integration.""" diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py new file mode 100644 index 0000000000000..40a6df853106e --- /dev/null +++ b/tests/components/frontier_silicon/conftest.py @@ -0,0 +1,59 @@ +"""Configuration for frontier_silicon tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.frontier_silicon.const import ( + CONF_PIN, + CONF_WEBFSAPI_URL, + DOMAIN, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry() -> MockConfigEntry: + """Create a mock Frontier Silicon config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="mock_radio_id", + data={CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", CONF_PIN: "1234"}, + ) + + +@pytest.fixture(autouse=True) +def mock_valid_device_url() -> Generator[None, None, None]: + """Return a valid webfsapi endpoint.""" + with patch( + "afsapi.AFSAPI.get_webfsapi_endpoint", + return_value="http://1.1.1.1:80/webfsapi", + ): + yield + + +@pytest.fixture(autouse=True) +def mock_valid_pin() -> Generator[None, None, None]: + """Make get_friendly_name return a value, indicating a valid pin.""" + with patch( + "afsapi.AFSAPI.get_friendly_name", + return_value="Name of the device", + ): + yield + + +@pytest.fixture(autouse=True) +def mock_radio_id() -> Generator[None, None, None]: + """Return a valid radio_id.""" + with patch("afsapi.AFSAPI.get_radio_id", return_value="mock_radio_id"): + yield + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.frontier_silicon.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/frontier_silicon/test_config_flow.py b/tests/components/frontier_silicon/test_config_flow.py new file mode 100644 index 0000000000000..a643b121c74d3 --- /dev/null +++ b/tests/components/frontier_silicon/test_config_flow.py @@ -0,0 +1,266 @@ +"""Test the Frontier Silicon config flow.""" +from unittest.mock import AsyncMock, patch + +from afsapi import ConnectionError, InvalidPinException +import pytest + +from homeassistant import config_entries +from homeassistant.components.frontier_silicon.const import CONF_WEBFSAPI_URL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_import_success(hass: HomeAssistant) -> None: + """Test successful import.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Test name" + assert result["data"] == { + CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", + CONF_PIN: "1234", + } + + +@pytest.mark.parametrize( + ("webfsapi_endpoint_error", "result_reason"), + [ + (ConnectionError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_import_webfsapi_endpoint_failures( + hass: HomeAssistant, webfsapi_endpoint_error: Exception, result_reason: str +) -> None: + """Test various failure of get_webfsapi_endpoint.""" + with patch( + "afsapi.AFSAPI.get_webfsapi_endpoint", + side_effect=webfsapi_endpoint_error, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == result_reason + + +@pytest.mark.parametrize( + ("radio_id_error", "result_reason"), + [ + (ConnectionError, "cannot_connect"), + (InvalidPinException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +async def test_import_radio_id_failures( + hass: HomeAssistant, radio_id_error: Exception, result_reason: str +) -> None: + """Test various failure of get_radio_id.""" + with patch( + "afsapi.AFSAPI.get_radio_id", + side_effect=radio_id_error, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == result_reason + + +async def test_import_already_exists( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test import of device which already exists.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 80, + CONF_PIN: "1234", + CONF_NAME: "Test name", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_form_default_pin( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test manual device add with default pin.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Name of the device" + assert result2["data"] == { + CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", + CONF_PIN: "1234", + } + mock_setup_entry.assert_called_once() + + +async def test_form_nondefault_pin( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "afsapi.AFSAPI.get_friendly_name", + side_effect=InvalidPinException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "device_config" + assert result2["errors"] is None + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PIN: "4321"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Name of the device" + assert result3["data"] == { + "webfsapi_url": "http://1.1.1.1:80/webfsapi", + "pin": "4321", + } + mock_setup_entry.assert_called_once() + + +@pytest.mark.parametrize( + ("friendly_name_error", "result_error"), + [ + (ConnectionError, "cannot_connect"), + (InvalidPinException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +async def test_form_nondefault_pin_invalid( + hass: HomeAssistant, friendly_name_error: Exception, result_error: str +) -> None: + """Test we get the proper errors when trying to validate an user-provided PIN.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "afsapi.AFSAPI.get_friendly_name", + side_effect=InvalidPinException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "device_config" + assert result2["errors"] is None + + with patch( + "afsapi.AFSAPI.get_friendly_name", + side_effect=friendly_name_error, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PIN: "4321"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.FORM + assert result2["step_id"] == "device_config" + assert result3["errors"] == {"base": result_error} + + +@pytest.mark.parametrize( + ("webfsapi_endpoint_error", "result_error"), + [ + (ConnectionError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_invalid_device_url( + hass: HomeAssistant, webfsapi_endpoint_error: Exception, result_error: str +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "afsapi.AFSAPI.get_webfsapi_endpoint", + side_effect=webfsapi_endpoint_error, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: 80}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": result_error}