diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index d9b3ea44fe14..4d73567d9f41 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -96,6 +96,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} self._cloud_username: str | None = None self._cloud_password: str | None = None + self._encryption_method_selected = False async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak @@ -197,6 +198,13 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): assert self._discovered_adv is not None description_placeholders: dict[str, str] = {} + if user_input is None: + if not self._encryption_method_selected and not ( + self._cloud_username and self._cloud_password + ): + return await self.async_step_encrypted_choose_method() + self._encryption_method_selected = False + # If we have saved credentials from cloud login, try them first if user_input is None and self._cloud_username and self._cloud_password: user_input = { @@ -258,6 +266,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the SwitchBot API chose method step.""" assert self._discovered_adv is not None + self._encryption_method_selected = True return self.async_show_menu( step_id="encrypted_choose_method", menu_options=["encrypted_auth", "encrypted_key"], @@ -272,6 +281,12 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the encryption key step.""" errors: dict[str, str] = {} assert self._discovered_adv is not None + + if user_input is None: + if not self._encryption_method_selected: + return await self.async_step_encrypted_choose_method() + self._encryption_method_selected = False + if user_input is not None: model: SwitchbotModel = self._discovered_adv.data["modelName"] cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model] diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 54aa37462e85..c2dbb8e59172 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -221,6 +221,113 @@ async def test_bluetooth_discovery_key(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_encrypted_key_back_navigation( + hass: HomeAssistant, +) -> None: + """Test that resuming an abandoned encrypted_key flow resets to the method menu.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOLOCK_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + # User selects encrypted_key + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_key"} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_key" + + # Simulate user closing dialog and re-opening: call the step with no input + # (as HA does when resuming an in-progress flow) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=None + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + # User can now pick a method again and complete the flow + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_key"} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_key" + + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "switchbot.SwitchbotLock.verify_encryption_key", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_discovery_encrypted_auth_back_navigation( + hass: HomeAssistant, +) -> None: + """Test that resuming an abandoned encrypted_auth flow resets to the method menu.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOLOCK_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + # User selects encrypted_auth + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_auth"} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_auth" + + # Simulate user closing dialog and re-opening: call the step with no input + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=None + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + # User can switch to encrypted_key and complete the flow + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_key"} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_key" + + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "switchbot.SwitchbotLock.verify_encryption_key", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device when already setup.""" entry = MockConfigEntry( @@ -1208,12 +1315,22 @@ async def test_user_cloud_login_then_encrypted_device(hass: HomeAssistant) -> No assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encrypted_auth" + # Simulate the user navigating away and re-opening the dialog. + # The failed auto-auth cleared credentials, so calling with None now + # redirects back to the method selection menu. result = await hass.config_entries.flow.async_configure( result["flow_id"], None, ) await hass.async_block_till_done() + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + # User selects encrypted_auth again and manually enters credentials + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_auth"} + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encrypted_auth"