From 9251dde2c6e11145990f149847e9dde83243d2d4 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 17 Oct 2025 07:27:11 -0300 Subject: [PATCH] Add OpenRGB reconfiguration flow (#154478) --- .../components/openrgb/config_flow.py | 49 ++++++ .../components/openrgb/quality_scale.yaml | 2 +- homeassistant/components/openrgb/strings.json | 14 ++ tests/components/openrgb/test_config_flow.py | 149 ++++++++++++++++++ 4 files changed, 213 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openrgb/config_flow.py b/homeassistant/components/openrgb/config_flow.py index fd82fdca726..687cfdd3f99 100644 --- a/homeassistant/components/openrgb/config_flow.py +++ b/homeassistant/components/openrgb/config_flow.py @@ -25,6 +25,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema( } ) +STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) + async def validate_input(hass: HomeAssistant, host: str, port: int) -> None: """Validate the user input allows us to connect.""" @@ -39,6 +46,48 @@ async def validate_input(hass: HomeAssistant, host: str, port: int) -> None: class OpenRGBConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OpenRGB.""" + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the OpenRGB SDK Server.""" + reconfigure_entry = self._get_reconfigure_entry() + + errors: dict[str, str] = {} + if user_input is not None: + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + # Prevent duplicate entries + self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) + + try: + await validate_input(self.hass, host, port) + except CONNECTION_ERRORS: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception( + "Unknown error while connecting to OpenRGB SDK server at %s", + f"{host}:{port}", + ) + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={CONF_HOST: host, CONF_PORT: port}, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_RECONFIGURE_DATA_SCHEMA, + suggested_values={ + CONF_HOST: reconfigure_entry.data[CONF_HOST], + CONF_PORT: reconfigure_entry.data[CONF_PORT], + }, + ), + errors=errors, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/openrgb/quality_scale.yaml b/homeassistant/components/openrgb/quality_scale.yaml index b969915020c..733671400d3 100644 --- a/homeassistant/components/openrgb/quality_scale.yaml +++ b/homeassistant/components/openrgb/quality_scale.yaml @@ -68,7 +68,7 @@ rules: entity-translations: todo exception-translations: done icon-translations: todo - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: todo stale-devices: todo diff --git a/homeassistant/components/openrgb/strings.json b/homeassistant/components/openrgb/strings.json index 908443cbef8..d6211191afb 100644 --- a/homeassistant/components/openrgb/strings.json +++ b/homeassistant/components/openrgb/strings.json @@ -2,6 +2,7 @@ "config": { "step": { "user": { + "title": "Set up OpenRGB SDK server", "description": "Set up your OpenRGB SDK server to allow control from within Home Assistant.", "data": { "name": "[%key:common::config_flow::data::name%]", @@ -13,6 +14,18 @@ "host": "The IP address or hostname of the computer running the OpenRGB SDK server.", "port": "The port number that the OpenRGB SDK server is running on." } + }, + "reconfigure": { + "title": "Reconfigure OpenRGB SDK server", + "description": "Update the connection settings for your OpenRGB SDK server.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "[%key:component::openrgb::config::step::user::data_description::host%]", + "port": "[%key:component::openrgb::config::step::user::data_description::port%]" + } } }, "error": { @@ -21,6 +34,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, diff --git a/tests/components/openrgb/test_config_flow.py b/tests/components/openrgb/test_config_flow.py index 8160f36fb11..ca798a1b060 100644 --- a/tests/components/openrgb/test_config_flow.py +++ b/tests/components/openrgb/test_config_flow.py @@ -112,3 +112,152 @@ async def test_user_flow_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_setup_entry", "mock_openrgb_client") +async def test_user_flow_duplicate_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test user flow when trying to add duplicate host/port combination.""" + mock_config_entry.add_to_hass(hass) + + # Create another config entry with different host/port + other_entry = MockConfigEntry( + domain=DOMAIN, + title="Other Computer", + data={ + CONF_NAME: "Other Computer", + CONF_HOST: "192.168.1.200", + CONF_PORT: 6743, + }, + entry_id="01J0EXAMPLE0CONFIGENTRY01", + ) + other_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + # Try to add entry with same host/port as other_entry + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Yet Another Computer", + CONF_HOST: "192.168.1.200", + CONF_PORT: 6743, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_setup_entry", "mock_openrgb_client") +async def test_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the reconfiguration flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.1.100", + CONF_PORT: 6743, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_HOST] == "192.168.1.100" + assert mock_config_entry.data[CONF_PORT] == 6743 + + +@pytest.mark.parametrize( + ("exception", "error_key"), + [ + (ConnectionRefusedError, "cannot_connect"), + (OpenRGBDisconnected, "cannot_connect"), + (TimeoutError, "cannot_connect"), + (socket.gaierror, "cannot_connect"), + (SDKVersionError, "cannot_connect"), + (RuntimeError("Test error"), "unknown"), + ], +) +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reconfigure_flow_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + exception: Exception, + error_key: str, + mock_openrgb_client, +) -> None: + """Test reconfiguration flow with various errors.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + mock_openrgb_client.client_class_mock.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "192.168.1.100", CONF_PORT: 6743}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": error_key} + + # Test recovery from error + mock_openrgb_client.client_class_mock.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "192.168.1.100", CONF_PORT: 6743}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_HOST] == "192.168.1.100" + assert mock_config_entry.data[CONF_PORT] == 6743 + + +@pytest.mark.usefixtures("mock_setup_entry", "mock_openrgb_client") +async def test_reconfigure_flow_duplicate_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test reconfiguration flow when new config matches another existing entry.""" + mock_config_entry.add_to_hass(hass) + + # Create another config entry with different host/port + other_entry = MockConfigEntry( + domain=DOMAIN, + title="Other Computer", + data={ + CONF_NAME: "Other Computer", + CONF_HOST: "192.168.1.200", + CONF_PORT: 6743, + }, + entry_id="01J0EXAMPLE0CONFIGENTRY01", + ) + other_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + # Try to reconfigure to match the other entry + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.1.200", + CONF_PORT: 6743, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured"