diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index b707ec62d3da67..c5b67ff16e8c94 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -14,8 +14,10 @@ RESULT_TYPE_ABORT = "abort" RESULT_TYPE_EXTERNAL_STEP = "external" RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" +RESULT_TYPE_SHOW_PROGRESS = "progress" +RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" -# Event that is fired when a flow is progressed via external source. +# Event that is fired when a flow is progressed via external or progress source. EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" @@ -152,8 +154,8 @@ async def async_configure( result = await self._async_handle_step(flow, cur_step["step_id"], user_input) - if cur_step["type"] == RESULT_TYPE_EXTERNAL_STEP: - if result["type"] not in ( + if cur_step["type"] in (RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_SHOW_PROGRESS): + if cur_step["type"] == RESULT_TYPE_EXTERNAL_STEP and result["type"] not in ( RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_EXTERNAL_STEP_DONE, ): @@ -161,10 +163,20 @@ async def async_configure( "External step can only transition to " "external step or external step done." ) + if cur_step["type"] == RESULT_TYPE_SHOW_PROGRESS and result["type"] not in ( + RESULT_TYPE_SHOW_PROGRESS, + RESULT_TYPE_SHOW_PROGRESS_DONE, + ): + raise ValueError( + "Show progress can only transition to show progress or show progress done." + ) # If the result has changed from last result, fire event to update # the frontend. - if cur_step["step_id"] != result.get("step_id"): + if ( + cur_step["step_id"] != result.get("step_id") + or result["type"] == RESULT_TYPE_SHOW_PROGRESS + ): # Tell frontend to reload the flow state. self.hass.bus.async_fire( EVENT_DATA_ENTRY_FLOW_PROGRESSED, @@ -217,6 +229,8 @@ async def _async_handle_step( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ABORT, RESULT_TYPE_EXTERNAL_STEP_DONE, + RESULT_TYPE_SHOW_PROGRESS, + RESULT_TYPE_SHOW_PROGRESS_DONE, ): raise ValueError(f"Handler returned incorrect type: {result['type']}") @@ -224,6 +238,8 @@ async def _async_handle_step( RESULT_TYPE_FORM, RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_EXTERNAL_STEP_DONE, + RESULT_TYPE_SHOW_PROGRESS, + RESULT_TYPE_SHOW_PROGRESS_DONE, ): flow.cur_step = result return result @@ -348,6 +364,34 @@ def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]: "step_id": next_step_id, } + @callback + def async_show_progress( + self, + *, + step_id: str, + progress_action: str, + description_placeholders: Optional[Dict] = None, + ) -> Dict[str, Any]: + """Show a progress message to the user, without user input allowed.""" + return { + "type": RESULT_TYPE_SHOW_PROGRESS, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": step_id, + "progress_action": progress_action, + "description_placeholders": description_placeholders, + } + + @callback + def async_show_progress_done(self, *, next_step_id: str) -> Dict[str, Any]: + """Mark the progress done.""" + return { + "type": RESULT_TYPE_SHOW_PROGRESS_DONE, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": next_step_id, + } + @callback def _create_abort_data( diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 64b8587fe7c9a1..b2fd9c8e34b43e 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -285,6 +285,76 @@ async def async_step_finish(self, user_input=None): assert result["title"] == "Hello" +async def test_show_progress(hass, manager): + """Test show progress logic.""" + manager.hass = hass + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + data = None + task_one_done = False + + async def async_step_init(self, user_input=None): + if not user_input: + if not self.task_one_done: + self.task_one_done = True + progress_action = "task_one" + else: + progress_action = "task_two" + return self.async_show_progress( + step_id="init", + progress_action=progress_action, + ) + + self.data = user_input + return self.async_show_progress_done(next_step_id="finish") + + async def async_step_finish(self, user_input=None): + return self.async_create_entry(title=self.data["title"], data=self.data) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED + ) + + result = await manager.async_init("test") + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["progress_action"] == "task_one" + assert len(manager.async_progress()) == 1 + + # Mimic task one done and moving to task two + # Called by integrations: `hass.config_entries.flow.async_configure(…)` + result = await manager.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["progress_action"] == "task_two" + + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data == { + "handler": "test", + "flow_id": result["flow_id"], + "refresh": True, + } + + # Mimic task two done and continuing step + # Called by integrations: `hass.config_entries.flow.async_configure(…)` + result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + + await hass.async_block_till_done() + assert len(events) == 2 + assert events[1].data == { + "handler": "test", + "flow_id": result["flow_id"], + "refresh": True, + } + + # Frontend refreshes the flow + result = await manager.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Hello" + + async def test_abort_flow_exception(manager): """Test that the AbortFlow exception works."""