diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e8c01e3f80a..412bc2ea4d9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -78,6 +78,8 @@ DISCOVERY_SOURCES = ( SOURCE_UNIGNORE, ) +RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" + EVENT_FLOW_DISCOVERED = "config_entry_discovered" CONN_CLASS_CLOUD_PUSH = "cloud_push" @@ -566,6 +568,15 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): ), notification_id=DISCOVERY_NOTIFICATION_ID, ) + elif source == SOURCE_REAUTH: + self.hass.components.persistent_notification.async_create( + title="Integration requires reconfiguration", + message=( + "At least one of your integrations requires reconfiguration to " + "continue functioning. [Check it out](/config/integrations)" + ), + notification_id=RECONFIGURE_NOTIFICATION_ID, + ) class ConfigEntries: @@ -1004,6 +1015,27 @@ class ConfigFlow(data_entry_flow.FlowHandler): await self._async_handle_discovery_without_unique_id() return await self.async_step_user() + @callback + def async_abort( + self, *, reason: str, description_placeholders: Optional[Dict] = None + ) -> Dict[str, Any]: + """Abort the config flow.""" + assert self.hass + + # Remove reauth notification if no reauth flows are in progress + if self.source == SOURCE_REAUTH and not any( + ent["context"]["source"] == SOURCE_REAUTH + for ent in self.hass.config_entries.flow.async_progress() + if ent["flow_id"] != self.flow_id + ): + self.hass.components.persistent_notification.async_dismiss( + RECONFIGURE_NOTIFICATION_ID + ) + + return super().async_abort( + reason=reason, description_placeholders=description_placeholders + ) + async_step_hassio = async_step_discovery async_step_homekit = async_step_discovery async_step_mqtt = async_step_discovery diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 840a7afc2ee..b707ec62d3d 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -261,11 +261,17 @@ class FlowHandler: @property def source(self) -> Optional[str]: """Source that initialized the flow.""" + if not hasattr(self, "context"): + return None + return self.context.get("source", None) @property def show_advanced_options(self) -> bool: """If we should show advanced options.""" + if not hasattr(self, "context"): + return False + return self.context.get("show_advanced_options", False) @callback diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index f13fac02850..6c2df29985c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -609,7 +609,8 @@ async def test_discovery_notification(hass): title="Test Title", data={"token": "abcd"} ) - result = await hass.config_entries.flow.async_init( + # Start first discovery flow to assert that reconfigure notification fires + flow1 = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY} ) @@ -617,11 +618,92 @@ async def test_discovery_notification(hass): state = hass.states.get("persistent_notification.config_entry_discovery") assert state is not None - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # Start a second discovery flow so we can finish the first and assert that + # the discovery notification persists until the second one is complete + flow2 = await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_DISCOVERY} + ) + + flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) + assert flow1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is not None + + flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) + assert flow2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is None + + +async def test_reauth_notification(hass): + """Test that we create/dismiss a notification when source is reauth.""" + mock_integration(hass, MockModule("test")) + mock_entity_platform(hass, "config_flow.test", None) + await async_setup_component(hass, "persistent_notification", {}) + + with patch.dict(config_entries.HANDLERS): + + class TestFlow(config_entries.ConfigFlow, domain="test"): + """Test flow.""" + + VERSION = 5 + + async def async_step_user(self, user_input): + """Test user step.""" + return self.async_show_form(step_id="user_confirm") + + async def async_step_user_confirm(self, user_input): + """Test user confirm step.""" + return self.async_show_form(step_id="user_confirm") + + async def async_step_reauth(self, user_input): + """Test reauth step.""" + return self.async_show_form(step_id="reauth_confirm") + + async def async_step_reauth_confirm(self, user_input): + """Test reauth confirm step.""" + return self.async_abort(reason="test") + + # Start user flow to assert that reconfigure notification doesn't fire + await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_USER} + ) + + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_reconfigure") + assert state is None + + # Start first reauth flow to assert that reconfigure notification fires + flow1 = await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_REAUTH} + ) + + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_reconfigure") + assert state is not None + + # Start a second reauth flow so we can finish the first and assert that + # the reconfigure notification persists until the second one is complete + flow2 = await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_REAUTH} + ) + + flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) + assert flow1["type"] == data_entry_flow.RESULT_TYPE_ABORT + + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_reconfigure") + assert state is not None + + flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) + assert flow2["type"] == data_entry_flow.RESULT_TYPE_ABORT + + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_reconfigure") assert state is None