Last active
March 6, 2026 08:40
-
-
Save AlCalzone/eb0947a39a3ff91c053f259f0ac4efc3 to your computer and use it in GitHub Desktop.
Helper script to migrate from the Z-Wave JS UI Community Home Assistant app to the updated Z-Wave JS app in version 1.0.0 (includes Z-Wave JS UI).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ########################################################################### | |
| # | |
| # Helper script to migrate from the Z-Wave JS UI Community Home Assistant app | |
| # to the updated Z-Wave JS app in version 1.0.0 (includes Z-Wave JS UI). | |
| # | |
| # Requires downloading a store backup from Z-Wave JS UI first | |
| # | |
| # ########################################################################### | |
| CORE_SLUG="core_zwave_js" | |
| ADDON_CONFIG="/addon_configs/$CORE_SLUG" | |
| CONFIG_ENTRIES="/homeassistant/.storage/core.config_entries" | |
| # --- Helpers --- | |
| log() { echo "[migrate] $*"; } | |
| die() { echo "[migrate] ERROR: $*" >&2; exit 1; } | |
| supervisor_call() { | |
| local method="$1" url="$2" body="${3:-}" | |
| local args=(-s -X "$method" -H "Authorization: Bearer ${SUPERVISOR_TOKEN}" -w "\n%{http_code}") | |
| if [[ -n "$body" ]]; then | |
| args+=(-H "Content-Type: application/json" -d "$body") | |
| fi | |
| local response | |
| response=$(curl "${args[@]}" "$url") | |
| local http_code | |
| http_code=$(echo "$response" | tail -n1) | |
| local response_body | |
| response_body=$(echo "$response" | sed '$d') | |
| if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then | |
| die "API call $method $url failed (HTTP $http_code): $response_body" | |
| fi | |
| echo "$response_body" | |
| } | |
| # --- Validate inputs --- | |
| if [[ $# -lt 1 ]]; then | |
| echo "Usage: $0 <backup_zip_path>" | |
| echo " backup_zip_path: Path to the store backup zip file" | |
| exit 1 | |
| fi | |
| BACKUP_ZIP="$1" | |
| if [[ ! -f "$BACKUP_ZIP" ]]; then | |
| die "Backup file not found: $BACKUP_ZIP" | |
| fi | |
| if [[ -z "${SUPERVISOR_TOKEN:-}" ]]; then | |
| die "SUPERVISOR_TOKEN is not set. This script must run in a Home Assistant app environment." | |
| fi | |
| # --- Determine home ID from config entries --- | |
| if [[ ! -f "$CONFIG_ENTRIES" ]]; then | |
| die "Config entries file not found: $CONFIG_ENTRIES" | |
| fi | |
| # Find zwave_js entries whose URL does not point to the core addon | |
| mapfile -t ENTRY_TITLES < <(jq -r ' | |
| .data.entries[] | |
| | select(.domain == "zwave_js") | |
| | select(.data.url != null) | |
| | select(.data.url | contains("core-zwave-js") | not) | |
| | .title | |
| ' "$CONFIG_ENTRIES") | |
| mapfile -t ENTRY_URLS < <(jq -r ' | |
| .data.entries[] | |
| | select(.domain == "zwave_js") | |
| | select(.data.url != null) | |
| | select(.data.url | contains("core-zwave-js") | not) | |
| | .data.url | |
| ' "$CONFIG_ENTRIES") | |
| mapfile -t ENTRY_IDS < <(jq -r ' | |
| .data.entries[] | |
| | select(.domain == "zwave_js") | |
| | select(.data.url != null) | |
| | select(.data.url | contains("core-zwave-js") | not) | |
| | .unique_id | |
| ' "$CONFIG_ENTRIES") | |
| if [[ ${#ENTRY_IDS[@]} -eq 0 ]]; then | |
| die "No non-core Z-Wave integration found in config entries." | |
| fi | |
| if [[ ${#ENTRY_IDS[@]} -eq 1 ]]; then | |
| DECIMAL_ID="${ENTRY_IDS[0]}" | |
| log "Found Z-Wave integration: ${ENTRY_TITLES[0]} (${ENTRY_URLS[0]})" | |
| else | |
| log "Multiple Z-Wave integrations found:" | |
| for i in "${!ENTRY_URLS[@]}"; do | |
| echo " $((i + 1))) ${ENTRY_TITLES[$i]} (${ENTRY_URLS[$i]})" | |
| done | |
| read -rp "Select the integration to migrate from [1-${#ENTRY_IDS[@]}]: " choice | |
| if [[ "$choice" -lt 1 || "$choice" -gt ${#ENTRY_IDS[@]} ]]; then | |
| die "Invalid selection: $choice" | |
| fi | |
| DECIMAL_ID="${ENTRY_IDS[$((choice - 1))]}" | |
| fi | |
| # Convert decimal home ID to lowercase hex | |
| HOME_ID=$(printf '%x' "$DECIMAL_ID") | |
| # --- Step 2: Unzip backup --- | |
| TEMP_DIR=$(mktemp -d) | |
| trap 'rm -rf "$TEMP_DIR"' EXIT | |
| log "Extracting backup to $TEMP_DIR..." | |
| unzip -q "$BACKUP_ZIP" -d "$TEMP_DIR" | |
| SETTINGS_FILE="$TEMP_DIR/settings.json" | |
| if [[ ! -f "$SETTINGS_FILE" ]]; then | |
| die "settings.json not found in backup" | |
| fi | |
| # --- Step 3: Extract security keys and port --- | |
| log "Reading security keys and port from settings.json..." | |
| S0_LEGACY=$(jq -r '.zwave.securityKeys.S0_Legacy // empty' "$SETTINGS_FILE") | |
| S2_ACCESS_CONTROL=$(jq -r '.zwave.securityKeys.S2_AccessControl // empty' "$SETTINGS_FILE") | |
| S2_AUTHENTICATED=$(jq -r '.zwave.securityKeys.S2_Authenticated // empty' "$SETTINGS_FILE") | |
| S2_UNAUTHENTICATED=$(jq -r '.zwave.securityKeys.S2_Unauthenticated // empty' "$SETTINGS_FILE") | |
| LR_S2_ACCESS_CONTROL=$(jq -r '.zwave.securityKeysLongRange.S2_AccessControl // empty' "$SETTINGS_FILE") | |
| LR_S2_AUTHENTICATED=$(jq -r '.zwave.securityKeysLongRange.S2_Authenticated // empty' "$SETTINGS_FILE") | |
| PORT=$(jq -r '.zwave.port // empty' "$SETTINGS_FILE") | |
| if [[ -z "$PORT" ]]; then | |
| die "No port found in settings.json" | |
| fi | |
| # --- Step 4: Verify cache files exist --- | |
| for suffix in ".jsonl" ".metadata.jsonl" ".values.jsonl"; do | |
| if [[ ! -f "$TEMP_DIR/${HOME_ID}${suffix}" ]]; then | |
| die "Cache file ${HOME_ID}${suffix} not found in backup. Is the home ID correct?" | |
| fi | |
| done | |
| # --- Step 5: Update core app config --- | |
| log "Configuring core app..." | |
| # Build options JSON | |
| if [[ "$PORT" == tcp://* || "$PORT" == esphome://* ]]; then | |
| DEVICE_KEY="socket" | |
| else | |
| DEVICE_KEY="device" | |
| fi | |
| OPTIONS=$(jq -n \ | |
| --arg s0 "$S0_LEGACY" \ | |
| --arg s2ac "$S2_ACCESS_CONTROL" \ | |
| --arg s2auth "$S2_AUTHENTICATED" \ | |
| --arg s2unauth "$S2_UNAUTHENTICATED" \ | |
| --arg lr_s2ac "$LR_S2_ACCESS_CONTROL" \ | |
| --arg lr_s2auth "$LR_S2_AUTHENTICATED" \ | |
| --arg nk "$S0_LEGACY" \ | |
| --arg port "$PORT" \ | |
| --arg dk "$DEVICE_KEY" \ | |
| '{ | |
| "options": { | |
| "s0_legacy_key": $s0, | |
| "s2_access_control_key": $s2ac, | |
| "s2_authenticated_key": $s2auth, | |
| "s2_unauthenticated_key": $s2unauth, | |
| "lr_s2_access_control_key": $lr_s2ac, | |
| "lr_s2_authenticated_key": $lr_s2auth, | |
| "network_key": $nk, | |
| ($dk): $port | |
| } | |
| }') | |
| supervisor_call POST "http://supervisor/addons/$CORE_SLUG/options" "$OPTIONS" > /dev/null | |
| # --- Step 6: Copy cache files --- | |
| log "Copying cache files..." | |
| mkdir -p "$ADDON_CONFIG/cache" | |
| for suffix in ".jsonl" ".metadata.jsonl" ".values.jsonl"; do | |
| cp "$TEMP_DIR/${HOME_ID}${suffix}" "$ADDON_CONFIG/cache/" | |
| done | |
| # --- Step 7: Copy nodes.json and users.json --- | |
| log "Copying nodes.json and users.json..." | |
| cp "$TEMP_DIR/nodes.json" "$ADDON_CONFIG/" | |
| cp "$TEMP_DIR/users.json" "$ADDON_CONFIG/" | |
| # --- Step 8: Transform and copy settings.json --- | |
| log "Transforming and copying settings.json..." | |
| jq ' | |
| # mqtt: set disabled to true, or create with just that | |
| if .mqtt then | |
| .mqtt.disabled = true | |
| else | |
| .mqtt = {disabled: true} | |
| end | | |
| # gateway: set log defaults and disable version notifications | |
| if .gateway then | |
| .gateway.logEnabled = false | | |
| .gateway.logLevel = "info" | | |
| .gateway.logToFile = false | | |
| .gateway.notifyNewVersions = false | |
| else | |
| .gateway = {logEnabled: false, logLevel: "info", logToFile: false, notifyNewVersions: false} | |
| end | | |
| # zwave: remove managed keys | |
| if .zwave then | |
| del( | |
| .zwave.logEnabled, | |
| .zwave.logLevel, | |
| .zwave.logToFile, | |
| .zwave.maxFiles, | |
| .zwave.logFilename, | |
| .zwave.forceConsole, | |
| .zwave.rf, | |
| .zwave.storage, | |
| .zwave.securityKeys, | |
| .zwave.securityKeysLongRange, | |
| .zwave.presets, | |
| .zwave.serverEnabled, | |
| .zwave.serverPort, | |
| .zwave.serverHost, | |
| .zwave.serverServiceDiscoveryDisabled, | |
| .zwave.enableSoftReset | |
| ) | |
| else | |
| . | |
| end | |
| ' "$SETTINGS_FILE" > "$ADDON_CONFIG/settings.json" | |
| # --- Step 9: Start core app --- | |
| log "Starting core app..." | |
| supervisor_call POST "http://supervisor/addons/$CORE_SLUG/start" > /dev/null | |
| log "Migration complete!" | |
| log "You can now reconfigure the Z-Wave integration as follows:" | |
| log "If using a USB or TCP based controller:" | |
| log " - CHECK 'Use the Z-Wave Supervisor app'" | |
| log "Otherwise:" | |
| log " - DO NOT check 'Use the Supervisor app'" | |
| log " - URL: ws://core-zwave-js:3000" | |
| log "" | |
| log "When done, you can remove your old Z-Wave JS UI app" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Changelog:
2026-02-23