diff --git a/homeassistant/components/onkyo/config_flow.py b/homeassistant/components/onkyo/config_flow.py index a6b3e20574d..4c5de362172 100644 --- a/homeassistant/components/onkyo/config_flow.py +++ b/homeassistant/components/onkyo/config_flow.py @@ -6,6 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, ConfigEntry, ConfigFlow, ConfigFlowResult, @@ -46,13 +47,11 @@ CONF_DEVICE = "device" INPUT_SOURCES_ALL_MEANINGS = [ input_source.value_meaning for input_source in InputSource ] +STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) STEP_CONFIGURE_SCHEMA = vol.Schema( { - vol.Required( - OPTION_VOLUME_RESOLUTION, - default=OPTION_VOLUME_RESOLUTION_DEFAULT, - ): vol.In(VOLUME_RESOLUTION_ALLOWED), - vol.Required(OPTION_INPUT_SOURCES, default=[]): SelectSelector( + vol.Required(OPTION_VOLUME_RESOLUTION): vol.In(VOLUME_RESOLUTION_ALLOWED), + vol.Required(OPTION_INPUT_SOURCES): SelectSelector( SelectSelectorConfig( options=INPUT_SOURCES_ALL_MEANINGS, multiple=True, @@ -96,15 +95,28 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: self._receiver_info = info + await self.async_set_unique_id( info.identifier, raise_on_progress=False ) - self._abort_if_unique_id_configured(updates=user_input) + if self.source == SOURCE_RECONFIGURE: + self._abort_if_unique_id_mismatch() + else: + self._abort_if_unique_id_configured() + return await self.async_step_configure_receiver() + 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="manual", - data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + data_schema=self.add_suggested_values_to_schema( + STEP_MANUAL_SCHEMA, suggested_values + ), errors=errors, ) @@ -160,6 +172,12 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the configuration of a single receiver.""" errors = {} + entry = None + entry_options = None + if self.source == SOURCE_RECONFIGURE: + entry = self._get_reconfigure_entry() + entry_options = entry.options + if user_input is not None: source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES] if not source_meanings: @@ -168,33 +186,80 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN): sources_store: dict[str, str] = {} for source_meaning in source_meanings: source = InputSource.from_meaning(source_meaning) - sources_store[source.value] = source_meaning - result = self.async_create_entry( - title=self._receiver_info.model_name, - data={ - CONF_HOST: self._receiver_info.host, - }, - options={ - OPTION_VOLUME_RESOLUTION: user_input[OPTION_VOLUME_RESOLUTION], - OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT, - OPTION_INPUT_SOURCES: sources_store, - }, - ) + source_name = source_meaning + if entry_options is not None: + source_name = entry_options[OPTION_INPUT_SOURCES].get( + source.value, source_name + ) + sources_store[source.value] = source_name + + volume_resolution = user_input[OPTION_VOLUME_RESOLUTION] + + if entry_options is None: + result = self.async_create_entry( + title=self._receiver_info.model_name, + data={ + CONF_HOST: self._receiver_info.host, + }, + options={ + OPTION_VOLUME_RESOLUTION: volume_resolution, + OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT, + OPTION_INPUT_SOURCES: sources_store, + }, + ) + else: + assert entry is not None + result = self.async_update_reload_and_abort( + entry, + data={ + CONF_HOST: self._receiver_info.host, + }, + options={ + OPTION_VOLUME_RESOLUTION: volume_resolution, + OPTION_MAX_VOLUME: entry_options[OPTION_MAX_VOLUME], + OPTION_INPUT_SOURCES: sources_store, + }, + ) + _LOGGER.debug("Configured receiver, result: %s", result) return result _LOGGER.debug("Configuring receiver, info: %s", self._receiver_info) + suggested_values = user_input + if suggested_values is None: + if entry_options is None: + suggested_values = { + OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT, + OPTION_INPUT_SOURCES: [], + } + else: + suggested_values = { + OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION], + OPTION_INPUT_SOURCES: [ + InputSource(input_source).value_meaning + for input_source in entry_options[OPTION_INPUT_SOURCES] + ], + } + return self.async_show_form( step_id="configure_receiver", - data_schema=STEP_CONFIGURE_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + STEP_CONFIGURE_SCHEMA, suggested_values + ), errors=errors, description_placeholders={ "name": f"{self._receiver_info.model_name} ({self._receiver_info.host})" }, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the receiver.""" + return await self.async_step_manual() + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" _LOGGER.debug("Import flow user input: %s", user_input) diff --git a/homeassistant/components/onkyo/strings.json b/homeassistant/components/onkyo/strings.json index 05d5852d29d..1b0eadcc45e 100644 --- a/homeassistant/components/onkyo/strings.json +++ b/homeassistant/components/onkyo/strings.json @@ -33,6 +33,8 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "unique_id_mismatch": "The serial number of the device does not match the previous serial number", "unknown": "[%key:common::config_flow::error::unknown%]" } }, diff --git a/tests/components/onkyo/__init__.py b/tests/components/onkyo/__init__.py index 9d57d4e887a..8900f189aea 100644 --- a/tests/components/onkyo/__init__.py +++ b/tests/components/onkyo/__init__.py @@ -19,9 +19,9 @@ def create_receiver_info(id: int) -> ReceiverInfo: ) -def create_empty_config_entry() -> MockConfigEntry: - """Create an empty config entry for use in unit tests.""" - config = {CONF_HOST: ""} +def create_config_entry_from_info(info: ReceiverInfo) -> MockConfigEntry: + """Create a config entry from receiver info.""" + data = {CONF_HOST: info.host} options = { "volume_resolution": 80, "input_sources": {"12": "tv"}, @@ -29,7 +29,25 @@ def create_empty_config_entry() -> MockConfigEntry: } return MockConfigEntry( - data=config, + data=data, + options=options, + title=info.model_name, + domain="onkyo", + unique_id=info.identifier, + ) + + +def create_empty_config_entry() -> MockConfigEntry: + """Create an empty config entry for use in unit tests.""" + data = {CONF_HOST: ""} + options = { + "volume_resolution": 80, + "input_sources": {"12": "tv"}, + "max_volume": 100, + } + + return MockConfigEntry( + data=data, options=options, title="Unit test Onkyo", domain="onkyo", diff --git a/tests/components/onkyo/test_config_flow.py b/tests/components/onkyo/test_config_flow.py index e13b61f47c4..f230ab124bd 100644 --- a/tests/components/onkyo/test_config_flow.py +++ b/tests/components/onkyo/test_config_flow.py @@ -8,13 +8,22 @@ import pytest from homeassistant import config_entries from homeassistant.components.onkyo import InputSource from homeassistant.components.onkyo.config_flow import OnkyoConfigFlow -from homeassistant.components.onkyo.const import DOMAIN +from homeassistant.components.onkyo.const import ( + DOMAIN, + OPTION_MAX_VOLUME, + OPTION_VOLUME_RESOLUTION, +) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType, InvalidData -from . import create_empty_config_entry, create_receiver_info, setup_integration +from . import ( + create_config_entry_from_info, + create_empty_config_entry, + create_receiver_info, + setup_integration, +) from tests.common import Mock, MockConfigEntry @@ -240,7 +249,7 @@ async def test_configure_empty_source_list(hass: HomeAssistant) -> None: configure_result = await hass.config_entries.flow.async_configure( select_result["flow_id"], - user_input={"input_sources": []}, + user_input={"volume_resolution": 200, "input_sources": []}, ) assert configure_result["errors"] == { @@ -273,13 +282,11 @@ async def test_configure_no_resolution(hass: HomeAssistant) -> None: user_input={CONF_HOST: "sample-host-name"}, ) - configure_result = await hass.config_entries.flow.async_configure( - select_result["flow_id"], - user_input={"input_sources": ["TV"]}, - ) - - assert configure_result["type"] is FlowResultType.CREATE_ENTRY - assert configure_result["options"]["volume_resolution"] == 50 + with pytest.raises(InvalidData): + await hass.config_entries.flow.async_configure( + select_result["flow_id"], + user_input={"input_sources": ["TV"]}, + ) async def test_configure_resolution_set(hass: HomeAssistant) -> None: @@ -295,25 +302,24 @@ async def test_configure_resolution_set(hass: HomeAssistant) -> None: {"next_step_id": "manual"}, ) - mock_info = Mock() - mock_info.identifier = "mock_id" + receiver_info = create_receiver_info(1) with patch( "homeassistant.components.onkyo.config_flow.async_interview", - return_value=mock_info, + return_value=receiver_info, ): select_result = await hass.config_entries.flow.async_configure( form_result["flow_id"], user_input={CONF_HOST: "sample-host-name"}, ) - configure_result = await hass.config_entries.flow.async_configure( - select_result["flow_id"], - user_input={"volume_resolution": 200, "input_sources": ["TV"]}, - ) + configure_result = await hass.config_entries.flow.async_configure( + select_result["flow_id"], + user_input={"volume_resolution": 200, "input_sources": ["TV"]}, + ) - assert configure_result["type"] is FlowResultType.CREATE_ENTRY - assert configure_result["options"]["volume_resolution"] == 200 + assert configure_result["type"] is FlowResultType.CREATE_ENTRY + assert configure_result["options"]["volume_resolution"] == 200 async def test_configure_invalid_resolution_set(hass: HomeAssistant) -> None: @@ -348,6 +354,73 @@ async def test_configure_invalid_resolution_set(hass: HomeAssistant) -> None: ) +async def test_reconfigure(hass: HomeAssistant) -> None: + """Test the reconfigure config flow.""" + receiver_info = create_receiver_info(1) + config_entry = create_config_entry_from_info(receiver_info) + await setup_integration(hass, config_entry, receiver_info) + + old_host = config_entry.data[CONF_HOST] + old_max_volume = config_entry.options[OPTION_MAX_VOLUME] + + result = await config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + + with patch( + "homeassistant.components.onkyo.config_flow.async_interview", + return_value=receiver_info, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"host": receiver_info.host} + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "configure_receiver" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={"volume_resolution": 200, "input_sources": ["TUNER"]}, + ) + + assert result3["type"] is FlowResultType.ABORT + assert result3["reason"] == "reconfigure_successful" + + assert config_entry.data[CONF_HOST] == old_host + assert config_entry.options[OPTION_VOLUME_RESOLUTION] == 200 + assert config_entry.options[OPTION_MAX_VOLUME] == old_max_volume + + +async def test_reconfigure_new_device(hass: HomeAssistant) -> None: + """Test the reconfigure config flow with new device.""" + receiver_info = create_receiver_info(1) + config_entry = create_config_entry_from_info(receiver_info) + await setup_integration(hass, config_entry, receiver_info) + + old_unique_id = receiver_info.identifier + + result = await config_entry.start_reconfigure_flow(hass) + + receiver_info_2 = create_receiver_info(2) + + with patch( + "homeassistant.components.onkyo.config_flow.async_interview", + return_value=receiver_info_2, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"host": receiver_info_2.host} + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "unique_id_mismatch" + + # unique id should remain unchanged + assert config_entry.unique_id == old_unique_id + + @pytest.mark.parametrize( ("user_input", "exception", "error"), [