Add Z-Wave JS NVM backup and restore API (#139233)

* ZWaveJS: NVM backup and restore API

* remove unused const

* test fix

* switch to WS commands

* Backup & restore MVP

* Use base64 data directly

* update tests

* fix mistake

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* PR comments

* update tests

* more tests

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Petar Petrov
2025-03-14 16:17:23 +02:00
committed by GitHub
parent e9c8b3acfc
commit de0efd61d1
2 changed files with 361 additions and 0 deletions

View File

@@ -5201,6 +5201,242 @@ async def test_get_integration_settings(
}
async def test_backup_nvm(
hass: HomeAssistant,
integration,
client,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the backup NVM websocket command."""
ws_client = await hass_ws_client(hass)
# Set up mocks for the controller events
controller = client.driver.controller
# Test subscription and events
with patch.object(
controller, "async_backup_nvm_raw_base64", return_value="test"
) as mock_backup:
# Send the subscription request
await ws_client.send_json_auto_id(
{
"type": "zwave_js/backup_nvm",
"entry_id": integration.entry_id,
}
)
# Verify the finished event with data first
msg = await ws_client.receive_json()
assert msg["type"] == "event"
assert msg["event"]["event"] == "finished"
assert msg["event"]["data"] == "test"
# Verify subscription success
msg = await ws_client.receive_json()
assert msg["type"] == "result"
assert msg["success"] is True
# Simulate progress events
event = Event(
"nvm backup progress",
{
"source": "controller",
"event": "nvm backup progress",
"bytesRead": 25,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm backup progress"
assert msg["event"]["bytesRead"] == 25
assert msg["event"]["total"] == 100
event = Event(
"nvm backup progress",
{
"source": "controller",
"event": "nvm backup progress",
"bytesRead": 50,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm backup progress"
assert msg["event"]["bytesRead"] == 50
assert msg["event"]["total"] == 100
# Wait for the backup to complete
await hass.async_block_till_done()
# Verify the backup was called
assert mock_backup.called
# Test backup failure
with patch.object(
controller,
"async_backup_nvm_raw_base64",
side_effect=FailedCommand("failed_command", "Backup failed"),
):
# Send the subscription request
await ws_client.send_json_auto_id(
{
"type": "zwave_js/backup_nvm",
"entry_id": integration.entry_id,
}
)
# Verify error response
msg = await ws_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "Backup failed"
# Test config entry not found
await ws_client.send_json_auto_id(
{
"type": "zwave_js/backup_nvm",
"entry_id": "invalid_entry_id",
}
)
msg = await ws_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "not_found"
# Test config entry not loaded
await hass.config_entries.async_unload(integration.entry_id)
await hass.async_block_till_done()
await ws_client.send_json_auto_id(
{
"type": "zwave_js/backup_nvm",
"entry_id": integration.entry_id,
}
)
msg = await ws_client.receive_json()
assert msg["error"]["code"] == "not_loaded"
async def test_restore_nvm(
hass: HomeAssistant,
integration,
client,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the restore NVM websocket command."""
ws_client = await hass_ws_client(hass)
# Set up mocks for the controller events
controller = client.driver.controller
# Test restore success
with patch.object(
controller, "async_restore_nvm_base64", return_value=None
) as mock_restore:
# Send the subscription request
await ws_client.send_json_auto_id(
{
"type": "zwave_js/restore_nvm",
"entry_id": integration.entry_id,
"data": "dGVzdA==", # base64 encoded "test"
}
)
# Verify the finished event first
msg = await ws_client.receive_json()
assert msg["type"] == "event"
assert msg["event"]["event"] == "finished"
# Verify subscription success
msg = await ws_client.receive_json()
assert msg["type"] == "result"
assert msg["success"] is True
# Simulate progress events
event = Event(
"nvm restore progress",
{
"source": "controller",
"event": "nvm restore progress",
"bytesWritten": 25,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm restore progress"
assert msg["event"]["bytesWritten"] == 25
assert msg["event"]["total"] == 100
event = Event(
"nvm restore progress",
{
"source": "controller",
"event": "nvm restore progress",
"bytesWritten": 50,
"total": 100,
},
)
controller.receive_event(event)
msg = await ws_client.receive_json()
assert msg["event"]["event"] == "nvm restore progress"
assert msg["event"]["bytesWritten"] == 50
assert msg["event"]["total"] == 100
# Wait for the restore to complete
await hass.async_block_till_done()
# Verify the restore was called
assert mock_restore.called
# Test restore failure
with patch.object(
controller,
"async_restore_nvm_base64",
side_effect=FailedCommand("failed_command", "Restore failed"),
):
# Send the subscription request
await ws_client.send_json_auto_id(
{
"type": "zwave_js/restore_nvm",
"entry_id": integration.entry_id,
"data": "dGVzdA==", # base64 encoded "test"
}
)
# Verify error response
msg = await ws_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "Restore failed"
# Test entry_id not found
await ws_client.send_json_auto_id(
{
"type": "zwave_js/restore_nvm",
"entry_id": "invalid_entry_id",
"data": "dGVzdA==", # base64 encoded "test"
}
)
msg = await ws_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "not_found"
# Test config entry not loaded
await hass.config_entries.async_unload(integration.entry_id)
await hass.async_block_till_done()
await ws_client.send_json_auto_id(
{
"type": "zwave_js/restore_nvm",
"entry_id": integration.entry_id,
"data": "dGVzdA==", # base64 encoded "test"
}
)
msg = await ws_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "not_loaded"
async def test_cancel_secure_bootstrap_s2(
hass: HomeAssistant, client, integration, hass_ws_client: WebSocketGenerator
) -> None: