From 97da8481d282eea927dcc26fd36a0e75f9c42214 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 13 Dec 2024 16:11:45 +0100 Subject: [PATCH] Add reconfigure flow to MQTT (#132246) * Add reconfigure flow for MQTT integration * Add test and translation strings * Update quality scale configuration * Do not cache ConfigEntry in flow * Make sorce condition explictit * Rework from suggested changes * Do not allow reconfigure_entry and reconfigure_entry_data to be `None` --- homeassistant/components/mqtt/config_flow.py | 34 +++++++++-- .../components/mqtt/quality_scale.yaml | 4 +- homeassistant/components/mqtt/strings.json | 1 + tests/components/mqtt/test_config_flow.py | 56 +++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 34d43ad87f3..ad3f3d35457 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, ConfigEntry, ConfigFlow, ConfigFlowResult, @@ -469,24 +470,41 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} fields: OrderedDict[Any, Any] = OrderedDict() validated_user_input: dict[str, Any] = {} + broker_config: dict[str, Any] = {} + if is_reconfigure := (self.source == SOURCE_RECONFIGURE): + reconfigure_entry = self._get_reconfigure_entry() if await async_get_broker_settings( self, fields, - None, + reconfigure_entry.data if is_reconfigure else None, user_input, validated_user_input, errors, ): + if is_reconfigure: + broker_config.update( + update_password_from_user_input( + reconfigure_entry.data.get(CONF_PASSWORD), validated_user_input + ), + ) + else: + broker_config = validated_user_input + can_connect = await self.hass.async_add_executor_job( try_connection, - validated_user_input, + broker_config, ) if can_connect: + if is_reconfigure: + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates=broker_config, + ) validated_user_input[CONF_DISCOVERY] = DEFAULT_DISCOVERY return self.async_create_entry( - title=validated_user_input[CONF_BROKER], - data=validated_user_input, + title=broker_config[CONF_BROKER], + data=broker_config, ) errors["base"] = "cannot_connect" @@ -495,6 +513,12 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): step_id="broker", data_schema=vol.Schema(fields), errors=errors ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + return await self.async_step_broker() + async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> ConfigFlowResult: @@ -547,7 +571,7 @@ class MQTTOptionsFlowHandler(OptionsFlow): def __init__(self) -> None: """Initialize MQTT options flow.""" - self.broker_config: dict[str, str | int] = {} + self.broker_config: dict[str, Any] = {} async def async_step_init(self, user_input: None = None) -> ConfigFlowResult: """Manage the MQTT options.""" diff --git a/homeassistant/components/mqtt/quality_scale.yaml b/homeassistant/components/mqtt/quality_scale.yaml index d1730d8d2fe..f31d3e25d15 100644 --- a/homeassistant/components/mqtt/quality_scale.yaml +++ b/homeassistant/components/mqtt/quality_scale.yaml @@ -90,9 +90,9 @@ rules: This is not possible because the integrations generates entities based on a user supplied config or discovery. reconfiguration-flow: - status: exempt + status: done comment: > - This integration is reconfigured via options flow. + This integration can also be reconfigured via options flow. dynamic-devices: status: done comment: | diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 4d23007e51b..c062c111487 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -101,6 +101,7 @@ "addon_connection_failed": "Failed to connect to the {addon} add-on. Check the add-on status and try again later.", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "error": { diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index e99063b088b..fc1221956de 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -2216,3 +2216,59 @@ async def test_change_websockets_transport_to_tcp( mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", } + + +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "test-broker", + CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, + mqtt.CONF_WS_PATH: "/some_path", + } + ], +) +async def test_reconfigure_flow_form( + hass: HomeAssistant, + mock_try_connection: MagicMock, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test reconfigure flow.""" + await mqtt_mock_entry() + entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + result = await hass.config_entries.flow.async_init( + mqtt.DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + "show_advanced_options": True, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "broker" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "10.10.10,10", + CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}', + mqtt.CONF_WS_PATH: "/some_new_path", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + mqtt.CONF_BROKER: "10.10.10,10", + CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, + mqtt.CONF_WS_PATH: "/some_new_path", + } + await hass.async_block_till_done(wait_background_tasks=True)