Fix Z-Wave unique id update during controller migration (#145185)

This commit is contained in:
Martin Hjelmare 2025-05-19 12:43:06 +02:00 committed by GitHub
parent 0fc81d6b33
commit 08104eec56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 376 additions and 73 deletions

View File

@ -105,6 +105,7 @@ from .const import (
CONF_USE_ADDON, CONF_USE_ADDON,
DATA_CLIENT, DATA_CLIENT,
DOMAIN, DOMAIN,
DRIVER_READY_TIMEOUT,
EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_DEVICE_ADDED_TO_REGISTRY,
EVENT_VALUE_UPDATED, EVENT_VALUE_UPDATED,
LIB_LOGGER, LIB_LOGGER,
@ -135,7 +136,6 @@ from .services import ZWaveServices
CONNECT_TIMEOUT = 10 CONNECT_TIMEOUT = 10
DATA_DRIVER_EVENTS = "driver_events" DATA_DRIVER_EVENTS = "driver_events"
DRIVER_READY_TIMEOUT = 60
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {

View File

@ -88,9 +88,9 @@ from .const import (
CONF_INSTALLER_MODE, CONF_INSTALLER_MODE,
DATA_CLIENT, DATA_CLIENT,
DOMAIN, DOMAIN,
DRIVER_READY_TIMEOUT,
EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_DEVICE_ADDED_TO_REGISTRY,
LOGGER, LOGGER,
RESTORE_NVM_DRIVER_READY_TIMEOUT,
USER_AGENT, USER_AGENT,
) )
from .helpers import ( 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 # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41
MINIMUM_QR_STRING_LENGTH = 52 MINIMUM_QR_STRING_LENGTH = 52
HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT = 60
# Helper schemas # Helper schemas
PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All(
@ -2866,7 +2864,7 @@ async def websocket_hard_reset_controller(
await driver.async_hard_reset() await driver.async_hard_reset()
with suppress(TimeoutError): 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() await wait_driver_ready.wait()
# When resetting the controller, the controller home id is also changed. # 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"]) await controller.async_restore_nvm_base64(msg["data"])
with suppress(TimeoutError): 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 wait_driver_ready.wait()
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)

View File

@ -65,7 +65,7 @@ from .const import (
CONF_USE_ADDON, CONF_USE_ADDON,
DATA_CLIENT, DATA_CLIENT,
DOMAIN, DOMAIN,
RESTORE_NVM_DRIVER_READY_TIMEOUT, DRIVER_READY_TIMEOUT,
) )
from .helpers import CannotConnect, async_get_version_info from .helpers import CannotConnect, async_get_version_info
@ -776,17 +776,14 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
) )
@callback @callback
def _async_update_entry( def _async_update_entry(self, updates: dict[str, Any]) -> None:
self, updates: dict[str, Any], *, schedule_reload: bool = True
) -> None:
"""Update the config entry with new data.""" """Update the config entry with new data."""
config_entry = self._reconfigure_config_entry config_entry = self._reconfigure_config_entry
assert config_entry is not None assert config_entry is not None
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
config_entry, data=config_entry.data | updates 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( async def async_step_intent_reconfigure(
self, user_input: dict[str, Any] | None = None 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 # Now that the old controller is gone, we can scan for serial ports again
return await self.async_step_choose_serial_port() 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 # reset the old controller
try: try:
await self._get_driver().async_hard_reset() await driver.async_hard_reset()
except (AbortFlow, FailedCommand) as err: except FailedCommand as err:
unsubscribe()
_LOGGER.error("Failed to reset controller: %s", err) _LOGGER.error("Failed to reset controller: %s", err)
return self.async_abort(reason="reset_failed") 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 config_entry = self._reconfigure_config_entry
assert config_entry is not None 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. # Unload the config entry before asking the user to unplug the controller.
await self.hass.config_entries.async_unload(config_entry.entry_id) 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 assert ws_address is not None
version_info = self.version_info version_info = self.version_info
assert version_info is not None 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, # We need to wait for the config entry to be reloaded,
# before restoring the backup. # before restoring the backup.
# We will do this in the restore nvm progress task, # We will do this in the restore nvm progress task,
# to get a nicer user experience. # to get a nicer user experience.
self._async_update_entry( self.hass.config_entries.async_update_entry(
{ config_entry,
"unique_id": str(version_info.home_id), data={
**config_entry.data,
CONF_URL: ws_address, CONF_URL: ws_address,
CONF_USB_PATH: self.usb_path, CONF_USB_PATH: self.usb_path,
CONF_S0_LEGACY_KEY: self.s0_legacy_key, CONF_S0_LEGACY_KEY: self.s0_legacy_key,
@ -1173,8 +1221,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_USE_ADDON: True, CONF_USE_ADDON: True,
CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, 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() return await self.async_step_restore_nvm()
async def async_step_finish_addon_setup_reconfigure( 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 raise AbortFlow(f"Failed to restore network: {err}") from err
else: else:
with suppress(TimeoutError): 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 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) await self.hass.config_entries.async_reload(config_entry.entry_id)
finally: finally:
for unsub in unsubs: for unsub in unsubs:

View File

@ -204,4 +204,4 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = {
# Other constants # Other constants
RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 DRIVER_READY_TIMEOUT = 60

View File

@ -5191,7 +5191,7 @@ async def test_hard_reset_controller(
client.async_send_command.side_effect = async_send_command_no_driver_ready client.async_send_command.side_effect = async_send_command_no_driver_ready
with patch( with patch(
"homeassistant.components.zwave_js.api.HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT", "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT",
new=0, new=0,
): ):
await ws_client.send_json_auto_id( 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 client.async_send_command.side_effect = async_send_command_no_driver_ready
with patch( with patch(
"homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT", "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT",
new=0, new=0,
): ):
# Send the subscription request # Send the subscription request

View File

@ -159,19 +159,6 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]:
client.driver.controller.data["sdkVersion"] = original_sdk_version 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: async def test_manual(hass: HomeAssistant) -> None:
"""Test we create an entry with manual step.""" """Test we create an entry with manual step."""
@ -867,8 +854,11 @@ async def test_usb_discovery_migration(
restart_addon: AsyncMock, restart_addon: AsyncMock,
client: MagicMock, client: MagicMock,
integration: MockConfigEntry, integration: MockConfigEntry,
get_server_version: AsyncMock,
) -> None: ) -> None:
"""Test usb discovery migration.""" """Test usb discovery migration."""
version_info = get_server_version.return_value
version_info.home_id = 4321
addon_options["device"] = "/dev/ttyUSB0" addon_options["device"] = "/dev/ttyUSB0"
entry = integration entry = integration
assert client.connect.call_count == 1 assert client.connect.call_count == 1
@ -893,6 +883,13 @@ async def test_usb_discovery_migration(
side_effect=mock_backup_nvm_raw 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): async def mock_restore_nvm(data: bytes):
client.driver.controller.emit( client.driver.controller.emit(
"nvm convert progress", "nvm convert progress",
@ -944,6 +941,7 @@ async def test_usb_discovery_migration(
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "instruct_unplug" assert result["step_id"] == "instruct_unplug"
assert entry.unique_id == "4321"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) 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") 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"]) result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.SHOW_PROGRESS 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["type"] is FlowResultType.ABORT
assert result["reason"] == "migration_successful" assert result["reason"] == "migration_successful"
assert integration.data["url"] == "ws://host1:3001" assert entry.data["url"] == "ws://host1:3001"
assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device
assert integration.data["use_addon"] is True assert entry.data["use_addon"] is True
assert entry.unique_id == "5678"
@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") @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, hass: HomeAssistant,
addon_options: dict[str, Any], addon_options: dict[str, Any],
driver_ready_timeout: None,
mock_usb_serial_by_id: MagicMock, mock_usb_serial_by_id: MagicMock,
set_addon_options: AsyncMock, set_addon_options: AsyncMock,
restart_addon: AsyncMock, restart_addon: AsyncMock,
@ -1030,6 +1030,13 @@ async def test_usb_discovery_migration_driver_ready_timeout(
side_effect=mock_backup_nvm_raw 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): async def mock_restore_nvm(data: bytes):
client.driver.controller.emit( client.driver.controller.emit(
"nvm convert progress", "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") 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["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "restore_nvm" assert result["step_id"] == "restore_nvm"
assert client.connect.call_count == 2 assert client.connect.call_count == 2
await hass.async_block_till_done() await hass.async_block_till_done()
assert client.connect.call_count == 3 assert client.connect.call_count == 3
assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.state is config_entries.ConfigEntryState.LOADED
assert client.driver.controller.async_restore_nvm.call_count == 1 assert client.driver.controller.async_restore_nvm.call_count == 1
assert len(events) == 2 assert len(events) == 2
assert events[0].data["progress"] == 0.25 assert events[0].data["progress"] == 0.25
assert events[1].data["progress"] == 0.75 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["type"] is FlowResultType.ABORT
assert result["reason"] == "migration_successful" 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( async def test_reconfigure_migrate_with_addon(
hass: HomeAssistant, hass: HomeAssistant,
client, client,
@ -3671,8 +3696,16 @@ async def test_reconfigure_migrate_with_addon(
restart_addon, restart_addon,
set_addon_options, set_addon_options,
get_addon_discovery_info, 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: ) -> None:
"""Test migration flow with add-on.""" """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 entry = integration
assert client.connect.call_count == 1 assert client.connect.call_count == 1
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
@ -3696,6 +3729,13 @@ async def test_reconfigure_migrate_with_addon(
side_effect=mock_backup_nvm_raw 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): async def mock_restore_nvm(data: bytes):
client.driver.controller.emit( client.driver.controller.emit(
"nvm convert progress", "nvm convert progress",
@ -3746,6 +3786,175 @@ async def test_reconfigure_migrate_with_addon(
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "instruct_unplug" assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED 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"], {}) 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") 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"]) result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.SHOW_PROGRESS 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["type"] is FlowResultType.ABORT
assert result["reason"] == "migration_successful" assert result["reason"] == "migration_successful"
assert integration.data["url"] == "ws://host1:3001" assert entry.data["url"] == "ws://host1:3001"
assert integration.data["usb_path"] == "/test" assert entry.data["usb_path"] == "/test"
assert integration.data["use_addon"] is True assert entry.data["use_addon"] is True
assert entry.unique_id == "5678"
@pytest.mark.parametrize( @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, hass: HomeAssistant,
client, client,
supervisor, supervisor,
integration, integration,
addon_running, addon_running,
driver_ready_timeout: None,
restart_addon, restart_addon,
set_addon_options, set_addon_options,
get_addon_discovery_info, get_addon_discovery_info,
@ -3841,6 +4052,13 @@ async def test_reconfigure_migrate_driver_ready_timeout(
side_effect=mock_backup_nvm_raw 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): async def mock_restore_nvm(data: bytes):
client.driver.controller.emit( client.driver.controller.emit(
"nvm convert progress", "nvm convert progress",
@ -3912,21 +4130,25 @@ async def test_reconfigure_migrate_driver_ready_timeout(
assert restart_addon.call_args == call("core_zwave_js") 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["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "restore_nvm" assert result["step_id"] == "restore_nvm"
assert client.connect.call_count == 2 assert client.connect.call_count == 2
await hass.async_block_till_done() await hass.async_block_till_done()
assert client.connect.call_count == 3 assert client.connect.call_count == 3
assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.state is config_entries.ConfigEntryState.LOADED
assert client.driver.controller.async_restore_nvm.call_count == 1 assert client.driver.controller.async_restore_nvm.call_count == 1
assert len(events) == 2 assert len(events) == 2
assert events[0].data["progress"] == 0.25 assert events[0].data["progress"] == 0.25
assert events[1].data["progress"] == 0.75 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["type"] is FlowResultType.ABORT
assert result["reason"] == "migration_successful" 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( client.driver.controller.async_backup_nvm_raw = AsyncMock(
side_effect=mock_backup_nvm_raw 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) 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( client.driver.controller.async_backup_nvm_raw = AsyncMock(
side_effect=mock_backup_nvm_raw 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( client.driver.controller.async_restore_nvm = AsyncMock(
side_effect=FailedCommand("test_error", "unknown_error") 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"]) result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.ABORT 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: 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 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) result = await entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.MENU assert result["type"] is FlowResultType.MENU