From 1437491e0a07a9dff3aa7a510ec517b5f7a17c98 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 9 Sep 2026 21:39:55 +0200 Subject: [PATCH] Centrally handle reconfigure successful for subentries (#181225) --- .../components/anthropic/strings.json | 6 +- .../components/bayesian/strings.json | 3 - .../components/fish_audio/config_flow.py | 1 + .../components/forecast_solar/strings.json | 3 +- .../strings.json | 12 ++-- .../components/google_weather/strings.json | 3 +- homeassistant/components/litellm/strings.json | 1 - homeassistant/components/mqtt/strings.json | 3 - homeassistant/components/ntfy/strings.json | 3 +- homeassistant/components/ollama/strings.json | 6 +- .../components/open_router/strings.json | 2 - .../openai_conversation/strings.json | 12 ++-- .../ovhcloud_ai_endpoints/strings.json | 1 - .../components/remember_the_milk/strings.json | 3 +- homeassistant/components/smtp/strings.json | 3 +- homeassistant/config_entries.py | 18 +++++- .../components/config/test_config_entries.py | 1 + tests/test_config_entries.py | 62 +++++++++++++++++++ 18 files changed, 97 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/anthropic/strings.json b/homeassistant/components/anthropic/strings.json index 4cbd0e1d4407..692173c28690 100644 --- a/homeassistant/components/anthropic/strings.json +++ b/homeassistant/components/anthropic/strings.json @@ -33,8 +33,7 @@ "config_subentries": { "ai_task_data": { "abort": { - "entry_not_loaded": "[%key:component::anthropic::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::anthropic::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "AI task", "error": { @@ -100,8 +99,7 @@ }, "conversation": { "abort": { - "entry_not_loaded": "Cannot add things while the configuration is disabled.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "Cannot add things while the configuration is disabled." }, "entry_type": "Conversation agent", "error": { diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json index c600956d0ae3..b6fad94a9f36 100644 --- a/homeassistant/components/bayesian/strings.json +++ b/homeassistant/components/bayesian/strings.json @@ -94,9 +94,6 @@ }, "config_subentries": { "observation": { - "abort": { - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" - }, "entry_type": "Observation", "error": { "above_below": "[%key:component::bayesian::config::error::above_below%]", diff --git a/homeassistant/components/fish_audio/config_flow.py b/homeassistant/components/fish_audio/config_flow.py index f10485761537..cd49c1f50ca5 100644 --- a/homeassistant/components/fish_audio/config_flow.py +++ b/homeassistant/components/fish_audio/config_flow.py @@ -362,6 +362,7 @@ class FishAudioSubentryFlowHandler(ConfigSubentryFlow): self._get_reconfigure_subentry(), data=self.config_data, unique_id=unique_id, + reason="reconfigure_successful", ) return self.async_show_form( diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index 8dcb91e89704..bfe1d7bff8a5 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -17,8 +17,7 @@ "plane": { "abort": { "api_key_required": "An API key is required to add more than one plane. You can configure it in the integration options.", - "max_planes": "You can add a maximum of 4 planes.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "max_planes": "You can add a maximum of 4 planes." }, "entry_type": "Plane", "initiate_flow": { diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index f3016921655f..3d523e9f549e 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -24,8 +24,7 @@ "config_subentries": { "ai_task_data": { "abort": { - "entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "AI task", "initiate_flow": { @@ -54,8 +53,7 @@ }, "conversation": { "abort": { - "entry_not_loaded": "Cannot add things while the configuration is disabled.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "Cannot add things while the configuration is disabled." }, "entry_type": "Conversation agent", "error": { @@ -96,8 +94,7 @@ }, "stt": { "abort": { - "entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "Speech-to-text", "initiate_flow": { @@ -130,8 +127,7 @@ }, "tts": { "abort": { - "entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "Text-to-speech", "initiate_flow": { diff --git a/homeassistant/components/google_weather/strings.json b/homeassistant/components/google_weather/strings.json index 2a4a30ffb0ba..789283740b14 100644 --- a/homeassistant/components/google_weather/strings.json +++ b/homeassistant/components/google_weather/strings.json @@ -38,8 +38,7 @@ "location": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", - "entry_not_loaded": "Cannot add things while the configuration is disabled.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "Cannot add things while the configuration is disabled." }, "entry_type": "Location", "error": { diff --git a/homeassistant/components/litellm/strings.json b/homeassistant/components/litellm/strings.json index c13cf5122080..6e03c07bbebc 100644 --- a/homeassistant/components/litellm/strings.json +++ b/homeassistant/components/litellm/strings.json @@ -27,7 +27,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "entry_not_loaded": "The main integration entry is not loaded. Please ensure the integration is loaded before reconfiguring.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "entry_type": "Conversation agent", diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 941575aa2934..c93dc0fb74e5 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -104,9 +104,6 @@ }, "config_subentries": { "device": { - "abort": { - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" - }, "create_entry": { "default": "MQTT device with {platform} entity \"{entity}\" was set up successfully.\n\nNote that you can reconfigure the MQTT device at any time, e.g. to add more entities." }, diff --git a/homeassistant/components/ntfy/strings.json b/homeassistant/components/ntfy/strings.json index 74b689627bec..69b27e28be64 100644 --- a/homeassistant/components/ntfy/strings.json +++ b/homeassistant/components/ntfy/strings.json @@ -83,8 +83,7 @@ "config_subentries": { "topic": { "abort": { - "already_configured": "Topic is already configured", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "already_configured": "Topic is already configured" }, "entry_type": "[%key:component::ntfy::common::topic%]", "error": { diff --git a/homeassistant/components/ollama/strings.json b/homeassistant/components/ollama/strings.json index e7ca74bf4439..d6631deb26e0 100644 --- a/homeassistant/components/ollama/strings.json +++ b/homeassistant/components/ollama/strings.json @@ -30,8 +30,7 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "download_failed": "[%key:component::ollama::config_subentries::conversation::abort::download_failed%]", - "entry_not_loaded": "[%key:component::ollama::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::ollama::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "AI task", "initiate_flow": { @@ -68,8 +67,7 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "download_failed": "Model downloading failed", - "entry_not_loaded": "Failed to add agent. The configuration is disabled.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "Failed to add agent. The configuration is disabled." }, "entry_type": "Conversation agent", "initiate_flow": { diff --git a/homeassistant/components/open_router/strings.json b/homeassistant/components/open_router/strings.json index be85e0ec3431..02650098021a 100644 --- a/homeassistant/components/open_router/strings.json +++ b/homeassistant/components/open_router/strings.json @@ -23,7 +23,6 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "entry_not_loaded": "The main integration entry is not loaded. Please ensure the integration is loaded before reconfiguring.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "entry_type": "AI task", @@ -44,7 +43,6 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "entry_not_loaded": "[%key:component::open_router::config_subentries::ai_task_data::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "entry_type": "Conversation agent", diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 6be23b94f0a8..1ab4611ca403 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -32,8 +32,7 @@ "config_subentries": { "ai_task_data": { "abort": { - "entry_not_loaded": "[%key:component::openai_conversation::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::openai_conversation::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "AI task", "error": { @@ -96,8 +95,7 @@ }, "conversation": { "abort": { - "entry_not_loaded": "Cannot add things while the configuration is disabled.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "Cannot add things while the configuration is disabled." }, "entry_type": "Conversation agent", "error": { @@ -165,8 +163,7 @@ }, "stt": { "abort": { - "entry_not_loaded": "[%key:component::openai_conversation::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::openai_conversation::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "Speech-to-text", "initiate_flow": { @@ -189,8 +186,7 @@ }, "tts": { "abort": { - "entry_not_loaded": "[%key:component::openai_conversation::config_subentries::conversation::abort::entry_not_loaded%]", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "[%key:component::openai_conversation::config_subentries::conversation::abort::entry_not_loaded%]" }, "entry_type": "Text-to-speech", "initiate_flow": { diff --git a/homeassistant/components/ovhcloud_ai_endpoints/strings.json b/homeassistant/components/ovhcloud_ai_endpoints/strings.json index fe120b0f0974..5b358bd8c982 100644 --- a/homeassistant/components/ovhcloud_ai_endpoints/strings.json +++ b/homeassistant/components/ovhcloud_ai_endpoints/strings.json @@ -42,7 +42,6 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "entry_not_loaded": "The main integration entry is not loaded. Please ensure the integration is loaded before configuring.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "entry_type": "Conversation agent", diff --git a/homeassistant/components/remember_the_milk/strings.json b/homeassistant/components/remember_the_milk/strings.json index ef8472e943f5..35419670a33d 100644 --- a/homeassistant/components/remember_the_milk/strings.json +++ b/homeassistant/components/remember_the_milk/strings.json @@ -38,8 +38,7 @@ "list": { "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "entry_not_loaded": "The Remember The Milk account must be loaded before adding or renaming a list.", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "entry_not_loaded": "The Remember The Milk account must be loaded before adding or renaming a list." }, "entry_type": "List", "error": { diff --git a/homeassistant/components/smtp/strings.json b/homeassistant/components/smtp/strings.json index 39e168d03878..4785419c11fc 100644 --- a/homeassistant/components/smtp/strings.json +++ b/homeassistant/components/smtp/strings.json @@ -86,8 +86,7 @@ "config_subentries": { "recipient": { "abort": { - "already_configured": "Recipient is already configured", - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "already_configured": "Recipient is already configured" }, "entry_type": "Recipient", "initiate_flow": { diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7bc2f23f6e7a..89077df7488d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3835,6 +3835,7 @@ class ConfigSubentryFlow( title: str | UndefinedType = UNDEFINED, data: Mapping[str, Any] | UndefinedType = UNDEFINED, data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED, + reason: str | UndefinedType = UNDEFINED, ) -> SubentryFlowResult: """Update config subentry and finish subentry flow. @@ -3843,6 +3844,8 @@ class ConfigSubentryFlow( keys are overridden :param title: replace the title of the subentry :param unique_id: replace the unique_id of the subentry + :param reason: set the reason for the abort, defaults to + `reconfigure_successful`. A custom reason requires a matching strings.json entry """ self._async_update( entry=entry, @@ -3852,7 +3855,11 @@ class ConfigSubentryFlow( data=data, data_updates=data_updates, ) - return self.async_abort(reason="reconfigure_successful") + translation_domain: str | None = None + if reason is UNDEFINED: + reason = "reconfigure_successful" + translation_domain = HOMEASSISTANT_DOMAIN + return self.async_abort(reason=reason, translation_domain=translation_domain) @callback def async_update_reload_and_abort( @@ -3864,6 +3871,7 @@ class ConfigSubentryFlow( title: str | UndefinedType = UNDEFINED, data: Mapping[str, Any] | UndefinedType = UNDEFINED, data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED, + reason: str | UndefinedType = UNDEFINED, reload_even_if_entry_is_unchanged: bool = True, ) -> SubentryFlowResult: """Update config subentry, reload config entry and finish subentry flow. @@ -3873,6 +3881,8 @@ class ConfigSubentryFlow( keys are overridden :param title: replace the title of the subentry :param unique_id: replace the unique_id of the subentry + :param reason: set the reason for the abort, defaults to + `reconfigure_successful`. A custom reason requires a matching strings.json entry :param reload_even_if_entry_is_unchanged: set this to `False` if the entry should not be reloaded if it is unchanged """ @@ -3888,7 +3898,11 @@ class ConfigSubentryFlow( if entry.update_listeners: raise ValueError("Cannot update and reload entry with update listeners") self.hass.config_entries.async_schedule_reload(entry.entry_id) - return self.async_abort(reason="reconfigure_successful") + translation_domain: str | None = None + if reason is UNDEFINED: + reason = "reconfigure_successful" + translation_domain = HOMEASSISTANT_DOMAIN + return self.async_abort(reason=reason, translation_domain=translation_domain) @property def _entry_id(self) -> str: diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 18472367c9c6..208790370480 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1729,6 +1729,7 @@ async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None: assert data == { "handler": ["test1", "test"], "reason": "reconfigure_successful", + "translation_domain": "homeassistant", "type": "abort", "description_placeholders": None, } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 240cc1b59fa7..d7f13c2aef5d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7746,6 +7746,62 @@ async def test_update_subentry_and_abort( else: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" + assert result["translation_domain"] == HOMEASSISTANT_DOMAIN + + +@pytest.mark.parametrize( + "update_and_abort", + ["async_update_and_abort", "async_update_reload_and_abort"], +) +async def test_update_subentry_custom_reason_stays_local( + hass: HomeAssistant, update_and_abort: str +) -> None: + """Test a caller supplied reason resolves against the integration itself.""" + subentry_id = "blabla" + entry = MockConfigEntry( + domain="comp", + unique_id="entry_unique_id", + title="entry_title", + data={}, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="1234", + title="Test", + ) + ], + ) + entry.add_to_hass(hass) + + mock_integration(hass, MockModule("comp")) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + async def async_step_reconfigure(self, user_input=None): + return getattr(self, update_and_abort)( + self._get_entry(), + self._get_reconfigure_subentry(), + reason="custom_reason", + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: config_entries.ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + with mock_config_flow("comp", TestFlow): + result = await entry.start_subentry_reconfigure_flow(hass, subentry_id) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "custom_reason" + assert "translation_domain" not in result @pytest.mark.parametrize( @@ -7775,6 +7831,7 @@ async def test_update_subentry_and_abort( { "type": FlowResultType.ABORT, "reason": "reconfigure_successful", + "translation_domain": HOMEASSISTANT_DOMAIN, "description_placeholders": None, }, ), @@ -7793,6 +7850,7 @@ async def test_update_subentry_and_abort( { "type": FlowResultType.ABORT, "reason": "reconfigure_successful", + "translation_domain": HOMEASSISTANT_DOMAIN, "description_placeholders": None, }, ), @@ -7811,6 +7869,7 @@ async def test_update_subentry_and_abort( { "type": FlowResultType.ABORT, "reason": "reconfigure_successful", + "translation_domain": HOMEASSISTANT_DOMAIN, "description_placeholders": None, }, ), @@ -7825,6 +7884,7 @@ async def test_update_subentry_and_abort( { "type": FlowResultType.ABORT, "reason": "reconfigure_successful", + "translation_domain": HOMEASSISTANT_DOMAIN, "description_placeholders": None, }, ), @@ -7841,6 +7901,7 @@ async def test_update_subentry_and_abort( { "type": FlowResultType.ABORT, "reason": "reconfigure_successful", + "translation_domain": HOMEASSISTANT_DOMAIN, "description_placeholders": None, }, ), @@ -7855,6 +7916,7 @@ async def test_update_subentry_and_abort( { "type": FlowResultType.ABORT, "reason": "reconfigure_successful", + "translation_domain": HOMEASSISTANT_DOMAIN, "description_placeholders": None, }, ),