diff --git a/homeassistant/components/russound_rio/config_flow.py b/homeassistant/components/russound_rio/config_flow.py index 15d002b3f49..e5efd309a23 100644 --- a/homeassistant/components/russound_rio/config_flow.py +++ b/homeassistant/components/russound_rio/config_flow.py @@ -9,7 +9,11 @@ from typing import Any from aiorussound import RussoundClient, RussoundTcpConnectionHandler import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers import config_validation as cv @@ -50,6 +54,12 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: await self.async_set_unique_id(controller.mac_address) + 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=user_input, + ) self._abort_if_unique_id_configured() data = {CONF_HOST: host, CONF_PORT: port} return self.async_create_entry( @@ -60,6 +70,17 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + if not user_input: + return self.async_show_form( + step_id="reconfigure", + data_schema=DATA_SCHEMA, + ) + return await self.async_step_user(user_input) + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: """Attempt to import the existing configuration.""" self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]}) diff --git a/homeassistant/components/russound_rio/quality_scale.yaml b/homeassistant/components/russound_rio/quality_scale.yaml index 3a5e8f9adb7..63693ee6259 100644 --- a/homeassistant/components/russound_rio/quality_scale.yaml +++ b/homeassistant/components/russound_rio/quality_scale.yaml @@ -11,10 +11,7 @@ rules: brands: done common-modules: done config-flow-test-coverage: done - config-flow: - status: todo - comment: | - The data_description fields in translations are missing. + config-flow: done dependency-transparency: done docs-actions: status: exempt @@ -65,7 +62,7 @@ rules: diagnostics: done exception-translations: done icon-translations: todo - reconfiguration-flow: todo + reconfiguration-flow: done dynamic-devices: todo discovery-update-info: todo repair-issues: done diff --git a/homeassistant/components/russound_rio/strings.json b/homeassistant/components/russound_rio/strings.json index b8c29c08301..93544064e20 100644 --- a/homeassistant/components/russound_rio/strings.json +++ b/homeassistant/components/russound_rio/strings.json @@ -9,6 +9,21 @@ "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]", "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The IP address of the Russound controller.", + "port": "The port of the Russound controller." + } + }, + "reconfigure": { + "description": "Reconfigure your Russound controller.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "[%key:component::russound_rio::config::step::user::data_description::host%]", + "port": "[%key:component::russound_rio::config::step::user::data_description::port%]" } } }, @@ -17,7 +32,9 @@ }, "abort": { "cannot_connect": "[%key:component::russound_rio::common::error_cannot_connect%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "wrong_device": "This Russound controller does not match the existing device id. Please make sure you entered the correct IP address." } }, "issues": { diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 5522c1e6ea2..3321d4160b9 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -9,9 +9,10 @@ from aiorussound.util import controller_device_str, zone_device_str import pytest from homeassistant.components.russound_rio.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from .const import API_VERSION, HARDWARE_MAC, HOST, MOCK_CONFIG, MODEL, PORT +from .const import API_VERSION, HARDWARE_MAC, MOCK_CONFIG, MODEL from tests.common import MockConfigEntry, load_json_object_fixture @@ -68,7 +69,9 @@ def mock_russound_client() -> Generator[AsyncMock]: 1, "MCA-C5", client, controller_device_str(1), HARDWARE_MAC, None, zones ) } - client.connection_handler = RussoundTcpConnectionHandler(HOST, PORT) + client.connection_handler = RussoundTcpConnectionHandler( + MOCK_CONFIG[CONF_HOST], MOCK_CONFIG[CONF_PORT] + ) client.is_connected = Mock(return_value=True) client.unregister_state_update_callbacks.return_value = True client.rio_version = API_VERSION diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 8f8ae7b59ea..18f75838525 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -3,16 +3,20 @@ from collections import namedtuple from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT -HOST = "127.0.0.1" -PORT = 9621 MODEL = "MCA-C5" HARDWARE_MAC = "00:11:22:33:44:55" API_VERSION = "1.08.00" MOCK_CONFIG = { - "host": HOST, - "port": PORT, + CONF_HOST: "192.168.20.75", + CONF_PORT: 9621, +} + +MOCK_RECONFIGURATION_CONFIG = { + CONF_HOST: "192.168.20.70", + CONF_PORT: 9622, } _CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) # noqa: PYI024 diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr index fcd59dd06f7..c92f06c4bc0 100644 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ b/tests/components/russound_rio/snapshots/test_init.ambr @@ -3,7 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , - 'configuration_url': 'http://127.0.0.1', + 'configuration_url': 'http://192.168.20.75', 'connections': set({ tuple( 'mac', diff --git a/tests/components/russound_rio/test_config_flow.py b/tests/components/russound_rio/test_config_flow.py index 28cbf7eda5e..7a3b7fac7da 100644 --- a/tests/components/russound_rio/test_config_flow.py +++ b/tests/components/russound_rio/test_config_flow.py @@ -3,11 +3,12 @@ from unittest.mock import AsyncMock from homeassistant.components.russound_rio.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import MOCK_CONFIG, MODEL +from .const import MOCK_CONFIG, MOCK_RECONFIGURATION_CONFIG, MODEL from tests.common import MockConfigEntry @@ -117,3 +118,63 @@ async def test_import_cannot_connect( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +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"] == "reconfigure" + + return reconfigure_result + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + + reconfigure_result = await _start_reconfigure_flow(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + MOCK_RECONFIGURATION_CONFIG, + ) + + 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: "192.168.20.70", + CONF_PORT: 9622, + } + + +async def test_reconfigure_unique_id_mismatch( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure reconfigure flow aborts when the bride changes.""" + mock_russound_client.controllers[1].mac_address = "different_mac" + + reconfigure_result = await _start_reconfigure_flow(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + MOCK_RECONFIGURATION_CONFIG, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device" diff --git a/tests/components/russound_rio/test_init.py b/tests/components/russound_rio/test_init.py index e7022fa6ac1..d654eea32bd 100644 --- a/tests/components/russound_rio/test_init.py +++ b/tests/components/russound_rio/test_init.py @@ -59,8 +59,8 @@ async def test_disconnect_reconnect_log( mock_russound_client.is_connected = Mock(return_value=False) await mock_state_update(mock_russound_client, CallbackType.CONNECTION) - assert "Disconnected from device at 127.0.0.1" in caplog.text + assert "Disconnected from device at 192.168.20.75" in caplog.text mock_russound_client.is_connected = Mock(return_value=True) await mock_state_update(mock_russound_client, CallbackType.CONNECTION) - assert "Reconnected to device at 127.0.0.1" in caplog.text + assert "Reconnected to device at 192.168.20.75" in caplog.text