Handle Z-Wave migration low SDK version (#143936)

This commit is contained in:
Martin Hjelmare 2025-04-30 10:43:05 +02:00 committed by GitHub
parent 98cbc2a182
commit 04bea9c732
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 60 additions and 1 deletions

View File

@ -9,6 +9,7 @@ from pathlib import Path
from typing import Any
import aiohttp
from awesomeversion import AwesomeVersion
from serial.tools import list_ports
import voluptuous as vol
from zwave_js_server.client import Client
@ -99,6 +100,7 @@ ADDON_USER_INPUT_MAP = {
}
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61")
def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
@ -810,6 +812,27 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
if not self._usb_discovery and not config_entry.data.get(CONF_USE_ADDON):
return self.async_abort(reason="addon_required")
try:
driver = self._get_driver()
except AbortFlow:
return self.async_abort(reason="config_entry_not_loaded")
if (
sdk_version := driver.controller.sdk_version
) is not None and sdk_version < MIN_MIGRATION_SDK_VERSION:
_LOGGER.warning(
"Migration from this controller that has SDK version %s "
"is not supported. If possible, update the firmware "
"of the controller to a firmware built using SDK version %s or higher",
sdk_version,
MIN_MIGRATION_SDK_VERSION,
)
return self.async_abort(
reason="migration_low_sdk_version",
description_placeholders={
"ok_sdk_version": str(MIN_MIGRATION_SDK_VERSION)
},
)
if user_input is not None:
self._migrating = True
return await self.async_step_backup_nvm()

View File

@ -12,8 +12,10 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"backup_failed": "Failed to back up network.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"config_entry_not_loaded": "The Z-Wave configuration entry is not loaded. Please try again when the configuration entry is loaded.",
"different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device.",
"discovery_requires_supervisor": "Discovery requires the supervisor.",
"migration_low_sdk_version": "The SDK version of the old controller is lower than {ok_sdk_version}. This means it's not possible to migrate the Non Volatile Memory (NVM) of the old controller to another controller.\n\nCheck the documentation on the manufacturer support pages of the old controller, if it's possible to upgrade the firmware of the old controller to a version that is build with SDK version {ok_sdk_version} or higher.",
"migration_successful": "Migration successful.",
"not_zwave_device": "Discovered device is not a Z-Wave device.",
"not_zwave_js_addon": "Discovered add-on is not the official Z-Wave add-on.",

View File

@ -180,6 +180,16 @@ def mock_usb_serial_by_id_fixture() -> Generator[MagicMock]:
yield mock_usb_serial_by_id
@pytest.fixture
def mock_sdk_version(client: MagicMock) -> Generator[None]:
"""Mock the SDK version of the controller."""
original_sdk_version = client.driver.controller.data.get("sdkVersion")
client.driver.controller.data["sdkVersion"] = "6.60"
yield
if original_sdk_version is not None:
client.driver.controller.data["sdkVersion"] = original_sdk_version
async def test_manual(hass: HomeAssistant) -> None:
"""Test we create an entry with manual step."""
@ -3478,6 +3488,30 @@ async def test_reconfigure_migrate_no_addon(hass: HomeAssistant, integration) ->
assert result["reason"] == "addon_required"
@pytest.mark.usefixtures("mock_sdk_version")
async def test_reconfigure_migrate_low_sdk_version(
hass: HomeAssistant,
integration: MockConfigEntry,
) -> None:
"""Test migration flow fails with too low controller SDK version."""
entry = integration
hass.config_entries.async_update_entry(
entry, unique_id="1234", data={**entry.data, "use_addon": True}
)
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.ABORT
assert result["reason"] == "migration_low_sdk_version"
@pytest.mark.parametrize(
"discovery_info",
[
@ -3906,7 +3940,7 @@ async def test_get_driver_failure(hass: HomeAssistant, integration, client) -> N
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "backup_failed"
assert result["reason"] == "config_entry_not_loaded"
async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: