diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index b92ff819701..bc0092d6953 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -10,7 +10,12 @@ from rokuecp import Roku, RokuError import voluptuous as vol from homeassistant.components import ssdp, zeroconf -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,20 +58,38 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = {} @callback - def _show_form(self, errors: dict[str, Any] | None = None) -> ConfigFlowResult: + def _show_form( + self, + user_input: dict[str, Any] | None, + errors: dict[str, Any] | None = None, + ) -> ConfigFlowResult: """Show the form to the user.""" + suggested_values = user_input + if suggested_values is None and self.source == SOURCE_RECONFIGURE: + suggested_values = { + CONF_HOST: self._get_reconfigure_entry().data[CONF_HOST] + } + return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + DATA_SCHEMA, suggested_values + ), errors=errors or {}, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + return await self.async_step_user(user_input) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if not user_input: - return self._show_form() + return self._show_form(user_input) errors = {} @@ -75,13 +98,21 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): except RokuError: _LOGGER.debug("Roku Error", exc_info=True) errors["base"] = ERROR_CANNOT_CONNECT - return self._show_form(errors) + return self._show_form(user_input, errors) except Exception: _LOGGER.exception("Unknown error trying to connect") return self.async_abort(reason=ERROR_UNKNOWN) await self.async_set_unique_id(info["serial_number"]) - self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) + + if self.source == SOURCE_RECONFIGURE: + self._abort_if_unique_id_mismatch(reason="wrong_device") + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), + data_updates={CONF_HOST: user_input[CONF_HOST]}, + ) + + self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 9d657be6d61..bd47585db1b 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -21,7 +21,9 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_device": "This Roku device does not match the existing device id. Please make sure you entered the correct host information." } }, "options": { diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 7144c77cad9..57ddf5d51a6 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,13 +1,18 @@ """Test the Roku config flow.""" import dataclasses -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import pytest -from rokuecp import RokuConnectionError +from rokuecp import Device as RokuDevice, RokuConnectionError from homeassistant.components.roku.const import CONF_PLAY_MEDIA_APP_ID, DOMAIN -from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_HOMEKIT, + SOURCE_SSDP, + SOURCE_USER, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -23,6 +28,8 @@ from . import ( from tests.common import MockConfigEntry +RECONFIGURE_HOST = "192.168.1.190" + async def test_duplicate_error( hass: HomeAssistant, @@ -276,3 +283,56 @@ async def test_options_flow( assert result2.get("data") == { CONF_PLAY_MEDIA_APP_ID: "782875", } + + +async def _start_reconfigure_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> ConfigFlowResult: + """Initialize a reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "user" + + return await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + {CONF_HOST: RECONFIGURE_HOST}, + ) + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_roku_config_flow: MagicMock, +) -> None: + """Test reconfigure flow.""" + result = await _start_reconfigure_flow(hass, mock_config_entry) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry + assert entry.data == { + CONF_HOST: RECONFIGURE_HOST, + } + + +async def test_reconfigure_unique_id_mismatch( + hass: HomeAssistant, + mock_device: RokuDevice, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_roku_config_flow: MagicMock, +) -> None: + """Ensure reconfigure flow aborts when the device changes.""" + mock_device.info.serial_number = "RECONFIG" + + result = await _start_reconfigure_flow(hass, mock_config_entry) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device"