diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index 8eae4ab49f2..e4fe71db9c6 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -1,20 +1,31 @@ """SFR Box config flow.""" from __future__ import annotations +from typing import Any + from sfrbox_api.bridge import SFRBox -from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector from homeassistant.helpers.httpx_client import get_async_client -from .const import DEFAULT_HOST, DOMAIN +from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): selector.TextSelector(), + } +) +AUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): selector.TextSelector(), + vol.Required(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), } ) @@ -23,6 +34,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): """SFR Box config flow.""" VERSION = 1 + _config: dict[str, Any] = {} + _box: SFRBox async def async_step_user( self, user_input: dict[str, str] | None = None @@ -39,8 +52,48 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(system_info.mac_addr) self._abort_if_unique_id_configured() self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - return self.async_create_entry(title="SFR Box", data=user_input) + self._box = box + self._config.update(user_input) + return await self.async_step_choose_auth() + data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=data_schema, errors=errors ) + + async def async_step_choose_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return self.async_show_menu( + step_id="choose_auth", + menu_options=["auth", "skip_auth"], + ) + + async def async_step_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Check authentication.""" + errors = {} + if user_input is not None: + try: + if (username := user_input[CONF_USERNAME]) and ( + password := user_input[CONF_PASSWORD] + ): + await self._box.authenticate(username=username, password=password) + except SFRBoxAuthenticationError: + errors["base"] = "invalid_auth" + else: + self._config.update(user_input) + return self.async_create_entry(title="SFR Box", data=self._config) + + data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input) + return self.async_show_form( + step_id="auth", data_schema=data_schema, errors=errors + ) + + async def async_step_skip_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Skip authentication.""" + return self.async_create_entry(title="SFR Box", data=self._config) diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py index bc7647bcc95..7a64994ce42 100644 --- a/homeassistant/components/sfr_box/const.py +++ b/homeassistant/components/sfr_box/const.py @@ -2,6 +2,7 @@ from homeassistant.const import Platform DEFAULT_HOST = "192.168.0.1" +DEFAULT_USERNAME = "admin" DOMAIN = "sfr_box" diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 12f5603c53a..094d3ccfda1 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -1,17 +1,32 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, "step": { + "auth": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, + "choose_auth": { + "description": "Setting credentials enables additional functionality.", + "menu_options": { + "auth": "Set credentials (recommended)", + "skip_auth": "Skip authentication" + } + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" - } + }, + "description": "Setting the credentials is optional, but enables additional functionality." } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "entity": { diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 8f6dacd3f19..82a2f9b6868 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -4,13 +4,28 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { + "auth": { + "data": { + "password": "Password", + "username": "Username" + } + }, + "choose_auth": { + "description": "Setting credentials enables additional functionality.", + "menu_options": { + "auth": "Set credentials (recommended)", + "skip_auth": "Skip authentication" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Setting the credentials is optional, but enables additional functionality." } } }, diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index ecdebad66e2..bca302f04af 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -3,12 +3,12 @@ import json from unittest.mock import AsyncMock, patch import pytest -from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError from sfrbox_api.models import SystemInfo from homeassistant import config_entries, data_entry_flow from homeassistant.components.sfr_box.const import DOMAIN -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import load_fixture @@ -23,7 +23,7 @@ def override_async_setup_entry() -> AsyncMock: yield mock_setup_entry -async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): +async def test_config_flow_skip_auth(hass: HomeAssistant, mock_setup_entry: AsyncMock): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -45,10 +45,11 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) with patch( "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", - return_value=system_info, + return_value=SystemInfo( + **json.loads(load_fixture("system_getInfo.json", DOMAIN)) + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -57,9 +58,81 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): }, ) + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "skip_auth"}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "SFR Box" - assert result["data"][CONF_HOST] == "192.168.0.1" + assert result["data"] == {CONF_HOST: "192.168.0.1"} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_flow_with_auth(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=SystemInfo( + **json.loads(load_fixture("system_getInfo.json", DOMAIN)) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "auth"}, + ) + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "invalid", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "valid", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SFR Box" + assert result["data"] == { + CONF_HOST: "192.168.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "valid", + } assert len(mock_setup_entry.mock_calls) == 1