diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 349baecc21d..6e76b2f89cf 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -105,6 +105,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, + DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_VALUE_UPDATED, LIB_LOGGER, @@ -135,7 +136,6 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_DRIVER_EVENTS = "driver_events" -DRIVER_READY_TIMEOUT = 60 CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index f480c822a8c..5f6050b88e9 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -88,9 +88,9 @@ from .const import ( CONF_INSTALLER_MODE, DATA_CLIENT, DOMAIN, + DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, - RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( @@ -189,8 +189,6 @@ STRATEGY = "strategy" # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41 MINIMUM_QR_STRING_LENGTH = 52 -HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT = 60 - # Helper schemas PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( @@ -2866,7 +2864,7 @@ async def websocket_hard_reset_controller( await driver.async_hard_reset() with suppress(TimeoutError): - async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() # When resetting the controller, the controller home id is also changed. @@ -3113,7 +3111,7 @@ async def websocket_restore_nvm( await controller.async_restore_nvm_base64(msg["data"]) with suppress(TimeoutError): - async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e52a5e784e8..e442fb59cfc 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -65,7 +65,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, - RESTORE_NVM_DRIVER_READY_TIMEOUT, + DRIVER_READY_TIMEOUT, ) from .helpers import CannotConnect, async_get_version_info @@ -776,17 +776,14 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) @callback - def _async_update_entry( - self, updates: dict[str, Any], *, schedule_reload: bool = True - ) -> None: + def _async_update_entry(self, updates: dict[str, Any]) -> None: """Update the config entry with new data.""" config_entry = self._reconfigure_config_entry assert config_entry is not None self.hass.config_entries.async_update_entry( config_entry, data=config_entry.data | updates ) - if schedule_reload: - self.hass.config_entries.async_schedule_reload(config_entry.entry_id) + self.hass.config_entries.async_schedule_reload(config_entry.entry_id) async def async_step_intent_reconfigure( self, user_input: dict[str, Any] | None = None @@ -896,15 +893,63 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): # Now that the old controller is gone, we can scan for serial ports again return await self.async_step_choose_serial_port() + try: + driver = self._get_driver() + except AbortFlow: + return self.async_abort(reason="config_entry_not_loaded") + + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + + unsubscribe = driver.once("driver ready", set_driver_ready) + # reset the old controller try: - await self._get_driver().async_hard_reset() - except (AbortFlow, FailedCommand) as err: + await driver.async_hard_reset() + except FailedCommand as err: + unsubscribe() _LOGGER.error("Failed to reset controller: %s", err) return self.async_abort(reason="reset_failed") + # Update the unique id of the config entry + # to the new home id, which requires waiting for the driver + # to be ready before getting the new home id. + # If the backup restore, done later in the flow, fails, + # the config entry unique id should be the new home id + # after the controller reset. + try: + async with asyncio.timeout(DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + except TimeoutError: + pass + finally: + unsubscribe() + config_entry = self._reconfigure_config_entry assert config_entry is not None + + try: + version_info = await async_get_version_info( + self.hass, config_entry.data[CONF_URL] + ) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded, if the backup restore + # also fails. + _LOGGER.debug( + "Failed to get server version, cannot update config entry " + "unique id with new home id, after controller reset" + ) + else: + self.hass.config_entries.async_update_entry( + config_entry, unique_id=str(version_info.home_id) + ) + # Unload the config entry before asking the user to unplug the controller. await self.hass.config_entries.async_unload(config_entry.entry_id) @@ -1154,14 +1199,17 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): assert ws_address is not None version_info = self.version_info assert version_info is not None + config_entry = self._reconfigure_config_entry + assert config_entry is not None # We need to wait for the config entry to be reloaded, # before restoring the backup. # We will do this in the restore nvm progress task, # to get a nicer user experience. - self._async_update_entry( - { - "unique_id": str(version_info.home_id), + self.hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, CONF_URL: ws_address, CONF_USB_PATH: self.usb_path, CONF_S0_LEGACY_KEY: self.s0_legacy_key, @@ -1173,8 +1221,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_USE_ADDON: True, CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, }, - schedule_reload=False, + unique_id=str(version_info.home_id), ) + return await self.async_step_restore_nvm() async def async_step_finish_addon_setup_reconfigure( @@ -1321,8 +1370,24 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): raise AbortFlow(f"Failed to restore network: {err}") from err else: with suppress(TimeoutError): - async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + try: + version_info = await async_get_version_info( + self.hass, config_entry.data[CONF_URL] + ) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded. + _LOGGER.error( + "Failed to get server version, cannot update config entry " + "unique id with new home id, after controller reset" + ) + else: + self.hass.config_entries.async_update_entry( + config_entry, unique_id=str(version_info.home_id) + ) await self.hass.config_entries.async_reload(config_entry.entry_id) finally: for unsub in unsubs: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 5792fca42a2..31cfb144e2a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -204,4 +204,4 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { # Other constants -RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 +DRIVER_READY_TIMEOUT = 60 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 7d4f9fe7a36..d2f0f205e8f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5191,7 +5191,7 @@ async def test_hard_reset_controller( client.async_send_command.side_effect = async_send_command_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", new=0, ): await ws_client.send_json_auto_id( @@ -5663,7 +5663,7 @@ async def test_restore_nvm( client.async_send_command.side_effect = async_send_command_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", new=0, ): # Send the subscription request diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 509fddb8704..7a2788a7b75 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -159,19 +159,6 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version -@pytest.fixture(name="driver_ready_timeout") -def mock_driver_ready_timeout() -> Generator[None]: - """Mock migration nvm restore driver ready timeout.""" - with patch( - ( - "homeassistant.components.zwave_js.config_flow." - "RESTORE_NVM_DRIVER_READY_TIMEOUT" - ), - new=0, - ): - yield - - async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -867,8 +854,11 @@ async def test_usb_discovery_migration( restart_addon: AsyncMock, client: MagicMock, integration: MockConfigEntry, + get_server_version: AsyncMock, ) -> None: """Test usb discovery migration.""" + version_info = get_server_version.return_value + version_info.home_id = 4321 addon_options["device"] = "/dev/ttyUSB0" entry = integration assert client.connect.call_count == 1 @@ -893,6 +883,13 @@ async def test_usb_discovery_migration( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -944,6 +941,7 @@ async def test_usb_discovery_migration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.unique_id == "4321" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -958,6 +956,8 @@ async def test_usb_discovery_migration( assert restart_addon.call_args == call("core_zwave_js") + version_info.home_id = 5678 + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS @@ -976,9 +976,10 @@ async def test_usb_discovery_migration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device + assert entry.data["use_addon"] is True + assert entry.unique_id == "5678" @pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") @@ -995,10 +996,9 @@ async def test_usb_discovery_migration( ] ], ) -async def test_usb_discovery_migration_driver_ready_timeout( +async def test_usb_discovery_migration_restore_driver_ready_timeout( hass: HomeAssistant, addon_options: dict[str, Any], - driver_ready_timeout: None, mock_usb_serial_by_id: MagicMock, set_addon_options: AsyncMock, restart_addon: AsyncMock, @@ -1030,6 +1030,13 @@ async def test_usb_discovery_migration_driver_ready_timeout( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -1092,21 +1099,25 @@ async def test_usb_discovery_migration_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + with patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "restore_nvm" - assert client.connect.call_count == 2 + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 - await hass.async_block_till_done() - assert client.connect.call_count == 3 - assert entry.state is config_entries.ConfigEntryState.LOADED - assert client.driver.controller.async_restore_nvm.call_count == 1 - assert len(events) == 2 - assert events[0].data["progress"] == 0.25 - assert events[1].data["progress"] == 0.75 + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" @@ -3662,6 +3673,20 @@ async def test_reconfigure_migrate_low_sdk_version( ] ], ) +@pytest.mark.parametrize( + ( + "reset_server_version_side_effect", + "reset_unique_id", + "restore_server_version_side_effect", + "final_unique_id", + ), + [ + (None, "4321", None, "8765"), + (aiohttp.ClientError("Boom"), "1234", None, "8765"), + (None, "4321", aiohttp.ClientError("Boom"), "5678"), + (aiohttp.ClientError("Boom"), "1234", aiohttp.ClientError("Boom"), "5678"), + ], +) async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, client, @@ -3671,8 +3696,16 @@ async def test_reconfigure_migrate_with_addon( restart_addon, set_addon_options, get_addon_discovery_info, + get_server_version: AsyncMock, + reset_server_version_side_effect: Exception | None, + reset_unique_id: str, + restore_server_version_side_effect: Exception | None, + final_unique_id: str, ) -> None: """Test migration flow with add-on.""" + get_server_version.side_effect = reset_server_version_side_effect + version_info = get_server_version.return_value + version_info.home_id = 4321 entry = integration assert client.connect.call_count == 1 hass.config_entries.async_update_entry( @@ -3696,6 +3729,13 @@ async def test_reconfigure_migrate_with_addon( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -3746,6 +3786,175 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.unique_id == reset_unique_id + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + assert result["data_schema"].schema[CONF_USB_PATH] + + # Reset side effect before starting the add-on. + get_server_version.side_effect = None + version_info.home_id = 5678 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", AddonsOptions(config={"device": "/test"}) + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert entry.unique_id == "5678" + get_server_version.side_effect = restore_server_version_side_effect + version_info.home_id = 8765 + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == final_unique_id + + +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_reconfigure_migrate_reset_driver_ready_timeout( + hass: HomeAssistant, + client, + supervisor, + integration, + addon_running, + restart_addon, + set_addon_options, + get_addon_discovery_info, + get_server_version: AsyncMock, +) -> None: + """Test migration flow with driver ready timeout after controller reset.""" + version_info = get_server_version.return_value + version_info.home_id = 4321 + entry = integration + assert client.connect.call_count == 1 + hass.config_entries.async_update_entry( + entry, + unique_id="1234", + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_reset_controller(): + await asyncio.sleep(0) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + + async def mock_restore_nvm(data: bytes): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + with ( + patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ), + patch("pathlib.Path.write_bytes") as mock_file, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.unique_id == "4321" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3770,6 +3979,8 @@ async def test_reconfigure_migrate_with_addon( assert restart_addon.call_args == call("core_zwave_js") + version_info.home_id = 5678 + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS @@ -3788,9 +3999,10 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == "/test" - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == "5678" @pytest.mark.parametrize( @@ -3806,13 +4018,12 @@ async def test_reconfigure_migrate_with_addon( ] ], ) -async def test_reconfigure_migrate_driver_ready_timeout( +async def test_reconfigure_migrate_restore_driver_ready_timeout( hass: HomeAssistant, client, supervisor, integration, addon_running, - driver_ready_timeout: None, restart_addon, set_addon_options, get_addon_discovery_info, @@ -3841,6 +4052,13 @@ async def test_reconfigure_migrate_driver_ready_timeout( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -3912,21 +4130,25 @@ async def test_reconfigure_migrate_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + with patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "restore_nvm" - assert client.connect.call_count == 2 + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 - await hass.async_block_till_done() - assert client.connect.call_count == 3 - assert entry.state is config_entries.ConfigEntryState.LOADED - assert client.driver.controller.async_restore_nvm.call_count == 1 - assert len(events) == 2 - assert events[0].data["progress"] == 0.25 - assert events[1].data["progress"] == 0.75 + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" @@ -4045,9 +4267,13 @@ async def test_reconfigure_migrate_start_addon_failure( client.driver.controller.async_backup_nvm_raw = AsyncMock( side_effect=mock_backup_nvm_raw ) - client.driver.controller.async_restore_nvm = AsyncMock( - side_effect=FailedCommand("test_error", "unknown_error") - ) + + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) result = await entry.start_reconfigure_flow(hass) @@ -4140,6 +4366,13 @@ async def test_reconfigure_migrate_restore_failure( client.driver.controller.async_backup_nvm_raw = AsyncMock( side_effect=mock_backup_nvm_raw ) + + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) client.driver.controller.async_restore_nvm = AsyncMock( side_effect=FailedCommand("test_error", "unknown_error") ) @@ -4292,7 +4525,7 @@ async def test_get_driver_failure_instruct_unplug( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reset_failed" + assert result["reason"] == "config_entry_not_loaded" async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: @@ -4358,6 +4591,13 @@ async def test_choose_serial_port_usb_ports_failure( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU