mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
e9c8b3acfc
commit
de0efd61d1
@ -454,6 +454,8 @@ def async_register_api(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_node_capabilities)
|
||||
websocket_api.async_register_command(hass, websocket_invoke_cc_api)
|
||||
websocket_api.async_register_command(hass, websocket_get_integration_settings)
|
||||
websocket_api.async_register_command(hass, websocket_backup_nvm)
|
||||
websocket_api.async_register_command(hass, websocket_restore_nvm)
|
||||
hass.http.register_view(FirmwareUploadView(dr.async_get(hass)))
|
||||
|
||||
|
||||
@ -2780,3 +2782,126 @@ def websocket_get_integration_settings(
|
||||
CONF_INSTALLER_MODE: hass.data[DOMAIN].get(CONF_INSTALLER_MODE, False),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/backup_nvm",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_backup_nvm(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
driver: Driver,
|
||||
) -> None:
|
||||
"""Backup NVM data."""
|
||||
controller = driver.controller
|
||||
|
||||
@callback
|
||||
def async_cleanup() -> None:
|
||||
"""Remove signal listeners."""
|
||||
for unsub in unsubs:
|
||||
unsub()
|
||||
|
||||
@callback
|
||||
def forward_progress(event: dict) -> None:
|
||||
"""Forward progress events to websocket."""
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg[ID],
|
||||
{
|
||||
"event": event["event"],
|
||||
"bytesRead": event["bytesRead"],
|
||||
"total": event["total"],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Set up subscription for progress events
|
||||
connection.subscriptions[msg["id"]] = async_cleanup
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
controller.on("nvm backup progress", forward_progress),
|
||||
]
|
||||
|
||||
result = await controller.async_backup_nvm_raw_base64()
|
||||
# Send the finished event with the backup data
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg[ID],
|
||||
{
|
||||
"event": "finished",
|
||||
"data": result,
|
||||
},
|
||||
)
|
||||
)
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/restore_nvm",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Required("data"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_restore_nvm(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
driver: Driver,
|
||||
) -> None:
|
||||
"""Restore NVM data."""
|
||||
controller = driver.controller
|
||||
|
||||
@callback
|
||||
def async_cleanup() -> None:
|
||||
"""Remove signal listeners."""
|
||||
for unsub in unsubs:
|
||||
unsub()
|
||||
|
||||
@callback
|
||||
def forward_progress(event: dict) -> None:
|
||||
"""Forward progress events to websocket."""
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg[ID],
|
||||
{
|
||||
"event": event["event"],
|
||||
"bytesRead": event.get("bytesRead"),
|
||||
"bytesWritten": event.get("bytesWritten"),
|
||||
"total": event["total"],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Set up subscription for progress events
|
||||
connection.subscriptions[msg["id"]] = async_cleanup
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
controller.on("nvm convert progress", forward_progress),
|
||||
controller.on("nvm restore progress", forward_progress),
|
||||
]
|
||||
|
||||
await controller.async_restore_nvm_base64(msg["data"])
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg[ID],
|
||||
{
|
||||
"event": "finished",
|
||||
},
|
||||
)
|
||||
)
|
||||
connection.send_result(msg[ID])
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user