Skip to content

Instantly share code, notes, and snippets.

@AlCalzone
Last active March 6, 2026 08:40
Show Gist options
  • Select an option

  • Save AlCalzone/eb0947a39a3ff91c053f259f0ac4efc3 to your computer and use it in GitHub Desktop.

Select an option

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).
#!/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"
@AlCalzone
Copy link
Author

Changelog:

2026-02-23

  • Disable Z-Wave JS UI's non-actionable upgrade notifications during the migration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment