diff --git a/config/templates/mupiboxconfig.json b/config/templates/mupiboxconfig.json index 12d6a89a..0d2dea2f 100644 --- a/config/templates/mupiboxconfig.json +++ b/config/templates/mupiboxconfig.json @@ -335,13 +335,25 @@ { "name": "ENERpower 2S2P 10.000mAh", "config": { - "v_100": "8000", + "v_100": "8200", "v_75": "7700", - "v_50": "7300", - "v_25": "6900", - "v_0": "6000", - "th_warning": "6500", - "th_shutdown": "6150" + "v_50": "7400", + "v_25": "7000", + "v_0": "6400", + "th_warning": "6700", + "th_shutdown": "6500" + } + }, + { + "name": "ENERpower 2S3P 15.000mAh", + "config": { + "v_100": "8200", + "v_75": "7700", + "v_50": "7400", + "v_25": "7000", + "v_0": "6400", + "th_warning": "6700", + "th_shutdown": "6500" } }, { diff --git a/scripts/dev/backup_box.sh b/scripts/dev/backup_box.sh new file mode 100644 index 00000000..7dd36746 --- /dev/null +++ b/scripts/dev/backup_box.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Pre-deploy backup of MuPiBox state. Run ON THE BOX (DietPi): +# +# ssh dietpi@ 'bash -s' < scripts/dev/backup_box.sh +# +# Creates /home/dietpi/mupibox-backup-/ containing every file the +# deploy will replace plus the user-state JSONs (resume.json, data.json, +# mupiboxconfig.json) so a full rollback is possible. The backup directory +# is on the SD card and survives reboots — clean up with `rm -rf` once a +# new release has proven itself. + +set -euo pipefail + +TS=$(date +%Y%m%d-%H%M%S) +BACKUP_DIR="/home/dietpi/mupibox-backup-${TS}" +mkdir -p "${BACKUP_DIR}" + +echo "==> Backup-Ziel: ${BACKUP_DIR}" + +# Code that the deploy will replace. +echo "--> backend-api (server.js + www/)" +cp -a /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server.js "${BACKUP_DIR}/" +cp -a /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www "${BACKUP_DIR}/www" + +echo "--> backend-player (spotify-control.js)" +cp -a /home/dietpi/.mupibox/spotifycontroller-main/spotify-control.js "${BACKUP_DIR}/" + +echo "--> Admin-Interface (PHP)" +sudo cp -a /var/www "${BACKUP_DIR}/admin-www" 2>/dev/null \ + || echo " (/var/www nicht vorhanden — überspringe)" + +echo "--> on-box Trim-Skripte" +sudo cp -a /usr/local/bin/mupibox/remove_max_resume.sh "${BACKUP_DIR}/" 2>/dev/null \ + || echo " (remove_max_resume.sh nicht vorhanden)" +sudo cp -a /usr/local/bin/mupibox/clearresume.sh "${BACKUP_DIR}/" 2>/dev/null \ + || echo " (clearresume.sh nicht vorhanden)" + +# User state — not touched by deploy, but back up anyway in case something +# goes sideways (e.g. self-heal misclassifies a working resume.json). +echo "--> User-State (resume.json, data.json, mupiboxconfig.json)" +cp -a /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/resume.json "${BACKUP_DIR}/" 2>/dev/null \ + || echo " (resume.json fehlt — frische Box?)" +cp -a /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/data.json "${BACKUP_DIR}/" +sudo cp -a /etc/mupibox/mupiboxconfig.json "${BACKUP_DIR}/" + +# pm2-Snapshot for rollback reproducibility. +echo "--> pm2-Status-Snapshot" +pm2 ls > "${BACKUP_DIR}/pm2-status.txt" 2>&1 || true + +sudo chown -R dietpi:dietpi "${BACKUP_DIR}" + +echo +echo "==> Inhalt:" +ls -lah "${BACKUP_DIR}" +echo +echo "==> Größe:" +du -sh "${BACKUP_DIR}" +echo +echo "==> Backup-Pfad (für rollback_box.sh notieren):" +echo " ${BACKUP_DIR}" diff --git a/scripts/dev/deploy_box.sh b/scripts/dev/deploy_box.sh new file mode 100644 index 00000000..f82f1461 --- /dev/null +++ b/scripts/dev/deploy_box.sh @@ -0,0 +1,185 @@ +#!/bin/bash +# +# Atomic-ish deploy of a freshly built deploy.zip to the box. Run ON THE BOX: +# +# bash deploy_box.sh /home/dietpi/mupibox-backup- +# +# Or pipe via SSH from the dev machine: +# +# cmd /c "ssh dietpi@ bash -s -- < scripts\dev\deploy_box.sh" +# +# Prerequisites (uploaded by the dev machine before this runs): +# /tmp/deploy.zip +# /tmp/remove_max_resume.sh (optional — only if changed) +# /tmp/clearresume.sh (optional — only if changed) +# +# Behaviour: +# - Verifies the backup exists (so rollback is possible) +# - Verifies deploy.zip contains the expected files (server.js, +# spotify-control.js, www/index.html) before touching anything +# - Restores the user-customised www/ files (active_theme.css, cover/, +# theme-data/) from the backup INTO the new www/ before swapping — +# theme + cover artwork + custom background survive the deploy +# - Restarts pm2 services +# - Cleans up its staging dir +# +# Does NOT touch user-state JSONs (resume.json, data.json, +# mupiboxconfig.json) — those persist across deploys by design. + +set -euo pipefail + +if [ "${#}" -lt 1 ]; then + echo "Usage: $0 /home/dietpi/mupibox-backup-" + echo + echo "Verfügbare Backups:" + ls -dt /home/dietpi/mupibox-backup-* 2>/dev/null || echo " (keine — erst backup_box.sh laufen lassen)" + exit 1 +fi + +BACKUP_DIR="${1}" +DEPLOY_ZIP="/tmp/deploy.zip" +TRIM_SCRIPTS_DIR="/tmp" + +if [ ! -d "${BACKUP_DIR}" ]; then + echo "FEHLER: Backup ${BACKUP_DIR} nicht gefunden. Erst backup_box.sh laufen lassen." + exit 1 +fi +if [ ! -f "${DEPLOY_ZIP}" ]; then + echo "FEHLER: ${DEPLOY_ZIP} fehlt. Erst per scp hochladen:" + echo " scp src/deploy.zip dietpi@:/tmp/" + exit 1 +fi + +WORK_DIR="/tmp/mupibox-deploy-staging-$(date +%s)" +mkdir -p "${WORK_DIR}" +trap 'rm -rf "${WORK_DIR}"' EXIT + +echo "==> Entpacke ${DEPLOY_ZIP} nach ${WORK_DIR}" +# PowerShell's Compress-Archive writes backslash separators which trips +# unzip's exit-1 "warning" — files still extract correctly, but set -e +# would otherwise abort the deploy. Treat exit codes 0 and 1 as success; +# 2+ are real failures (CRC, missing files, etc.). +set +e +unzip -q "${DEPLOY_ZIP}" -d "${WORK_DIR}" +unzip_rc=$? +set -e +if [ "${unzip_rc}" -ge 2 ]; then + echo "FEHLER: unzip fehlgeschlagen mit Exit-Code ${unzip_rc}" + exit 1 +fi + +for f in "${WORK_DIR}/server.js" "${WORK_DIR}/spotify-control.js" "${WORK_DIR}/www/index.html"; do + if [ ! -f "${f}" ]; then + echo "FEHLER: ${f} fehlt nach dem Entpacken — Build kaputt oder ZIP-Pfade falsch?" + echo " Inhalt von ${WORK_DIR}:" + ls -la "${WORK_DIR}" + exit 1 + fi +done +echo " server.js, spotify-control.js, www/index.html vorhanden ✓" + +echo "==> Stoppe Services" +pm2 stop server spotify-control || true + +echo "==> Übernehme User-Anpassungen aus dem Backup ins neue www/" +# active_theme.css: vom Admin-UI generiertes Theme +# cover/ : Cover lokaler Hörbücher (oder Symlink auf MuPiBox/media) +# theme-data/ : Hintergrund-Bilder für Custom-Themes +for item in active_theme.css cover theme-data; do + if [ -e "${BACKUP_DIR}/www/${item}" ]; then + cp -a "${BACKUP_DIR}/www/${item}" "${WORK_DIR}/www/" + echo " ${item} aus Backup übernommen" + else + echo " (${item} nicht im Backup, überspringe)" + fi +done + +echo "==> Backend-API: server.js + www/" +cp -f "${WORK_DIR}/server.js" /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server.js +rm -rf /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www +cp -a "${WORK_DIR}/www" /home/dietpi/.mupibox/Sonos-Kids-Controller-master/ + +echo "==> Backend-Player: spotify-control.js" +cp -f "${WORK_DIR}/spotify-control.js" /home/dietpi/.mupibox/spotifycontroller-main/spotify-control.js + +echo "==> Trim-Skripte (falls in ${TRIM_SCRIPTS_DIR})" +for s in remove_max_resume.sh clearresume.sh; do + if [ -f "${TRIM_SCRIPTS_DIR}/${s}" ]; then + sudo install -m 755 -o root -g root "${TRIM_SCRIPTS_DIR}/${s}" /usr/local/bin/mupibox/ + echo " ${s} installiert" + else + echo " (${s} nicht in ${TRIM_SCRIPTS_DIR} — überspringe)" + fi +done + +echo "==> Admin-Interface PHP (falls smart.php in ${TRIM_SCRIPTS_DIR})" +if [ -f "${TRIM_SCRIPTS_DIR}/smart.php" ]; then + sudo install -m 644 -o dietpi -g dietpi "${TRIM_SCRIPTS_DIR}/smart.php" /var/www/smart.php + echo " smart.php installiert" +else + echo " (smart.php nicht in ${TRIM_SCRIPTS_DIR} — überspringe)" +fi + +# Phase-1 Critical-Lockdown: 8 PHP files + 2 Bluetooth scripts. The PHP +# files live partly in /var/www/ and partly in /var/www/includes/, so the +# uploader stages everything under /tmp/admin_phase1/ preserving that +# layout. Allowlisted by name — never blow away /var/www/* wholesale. +echo "==> Admin-Phase-1 PHP (falls /tmp/admin_phase1/ existiert)" +if [ -d "${TRIM_SCRIPTS_DIR}/admin_phase1" ]; then + for f in admin.php backup.php fullbackup.php debug.php pm2logs.php support_data.php backend.php jsoneditor.php; do + if [ -f "${TRIM_SCRIPTS_DIR}/admin_phase1/${f}" ]; then + sudo install -m 644 -o dietpi -g dietpi "${TRIM_SCRIPTS_DIR}/admin_phase1/${f}" "/var/www/${f}" + echo " ${f} installiert" + fi + done + if [ -f "${TRIM_SCRIPTS_DIR}/admin_phase1/includes/header.php" ]; then + sudo install -m 644 -o dietpi -g dietpi "${TRIM_SCRIPTS_DIR}/admin_phase1/includes/header.php" /var/www/includes/header.php + echo " includes/header.php installiert" + fi +else + echo " (admin_phase1 nicht in ${TRIM_SCRIPTS_DIR} — überspringe)" +fi + +echo "==> Bluetooth-Skripte (falls ${TRIM_SCRIPTS_DIR}/{pair,remove}_bt.sh)" +for s in pair_bt.sh remove_bt.sh; do + if [ -f "${TRIM_SCRIPTS_DIR}/${s}" ]; then + sudo install -m 755 -o root -g root "${TRIM_SCRIPTS_DIR}/${s}" /usr/local/bin/mupibox/ + echo " ${s} installiert" + fi +done + +echo "==> Telegram-Skripte (falls in ${TRIM_SCRIPTS_DIR}/telegram_*.py)" +shopt -s nullglob +telegram_files=("${TRIM_SCRIPTS_DIR}"/telegram_*.py) +shopt -u nullglob +if [ "${#telegram_files[@]}" -gt 0 ]; then + for s in "${telegram_files[@]}"; do + sudo install -m 755 -o root -g root "${s}" /usr/local/bin/mupibox/ + echo " $(basename "${s}") installiert" + done + # Restart the receiver so the new code (multi-chat-auth, /limit set) takes effect. + if systemctl list-unit-files mupi_telegram.service >/dev/null 2>&1; then + sudo systemctl restart mupi_telegram.service \ + && echo " mupi_telegram.service neu gestartet" \ + || echo " WARN: mupi_telegram.service-Restart fehlgeschlagen" + fi +else + echo " (keine telegram_*.py in ${TRIM_SCRIPTS_DIR} — überspringe)" +fi + +echo "==> Starte Services" +pm2 start server spotify-control +pm2 status + +echo +echo "==> Deploy fertig. Jetzt:" +echo " 1. Browser/Kiosk auf der Box neu laden (Touchscreen Reload-Geste oder Reboot)" +echo " 2. Live-Verifikation:" +echo " - Spotify-Resume-Karte → Position?" +echo " - Library-Resume bei Track 10+ → kein Tick-Tick?" +echo " - Cap-Transition während Home-Page → Resume-Karte danach da?" +echo " - Hörbuch durchgelaufen → Resume-Karte weg?" +echo " - Alte Resume-Einträge → klickbar?" +echo +echo "==> Bei Problemen Rollback:" +echo " bash restore_box.sh ${BACKUP_DIR}" diff --git a/scripts/dev/deploy_phase1.ps1 b/scripts/dev/deploy_phase1.ps1 new file mode 100644 index 00000000..7f11d99a --- /dev/null +++ b/scripts/dev/deploy_phase1.ps1 @@ -0,0 +1,146 @@ +# Phase-1-Deployment-Wrapper. +# +# Builds a fresh deploy.zip (so spotify-control.js carries the CRIT-8 fix), +# stages the Phase-1 PHP files and BT scripts under a temp dir mirroring +# the on-box layout, scp's everything to /tmp/ on the box, then runs +# scripts/dev/backup_box.sh and scripts/dev/deploy_box.sh remotely. +# +# Usage (from the repo root, in PowerShell): +# +# .\scripts\dev\deploy_phase1.ps1 -Box dietpi@10.4.22.21 +# +# Or skip the build if deploy.zip is already current: +# +# .\scripts\dev\deploy_phase1.ps1 -Box dietpi@10.4.22.21 -SkipBuild +# +# Flags: +# -SkipBuild Reuse src/deploy.zip as-is (debugging / re-runs). +# -DryRun Print every step but execute nothing. +# +# This script does NOT push to the box without a backup first — that is +# the deploy_box.sh contract (it requires an existing backup dir as $1). + +param( + [Parameter(Mandatory=$true)] + [string]$Box, + [switch]$SkipBuild, + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') +Set-Location $repoRoot + +function Run([string]$desc, [scriptblock]$block) { + Write-Host "" + Write-Host "==> $desc" -ForegroundColor Cyan + if ($DryRun) { + Write-Host " (dry-run, would execute):" -ForegroundColor Yellow + Write-Host " $($block.ToString().Trim())" -ForegroundColor Yellow + return + } + & $block + if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) { + throw "$desc failed (exit $LASTEXITCODE)" + } +} + +# --- 1. Build deploy.zip ---------------------------------------------------- +if (-not $SkipBuild) { + Run "Build deploy.zip (frontend-box + backend-api + backend-player)" { + Set-Location (Join-Path $repoRoot 'src') + if (Test-Path .\deploy) { Remove-Item .\deploy -Recurse -Force } + if (Test-Path .\deploy.zip) { Remove-Item .\deploy.zip -Force } + + Push-Location frontend-box + npm run build + Pop-Location + + Push-Location backend-api + npm run build + Pop-Location + + Push-Location backend-player + npm run build + Pop-Location + + # Angular 20 emits to www/browser/ — flatten into www/ + Get-ChildItem .\deploy\www\browser\* -Recurse | Move-Item -Destination .\deploy\www\ + Remove-Item .\deploy\www\browser -Recurse -Force + Copy-Item .\backend-player\README.md .\deploy\README.md + Compress-Archive -Path .\deploy\* -DestinationPath .\deploy.zip + Set-Location $repoRoot + } +} else { + Write-Host "==> Build übersprungen (-SkipBuild). src/deploy.zip muss aktuell sein." -ForegroundColor Yellow + if (-not (Test-Path src/deploy.zip)) { + throw "src/deploy.zip fehlt. Lass -SkipBuild weg, damit es gebaut wird." + } +} + +# --- 2. Stage Phase-1 files ------------------------------------------------- +$staging = Join-Path $env:TEMP "mupibox-phase1-$(Get-Date -Format 'yyyyMMdd-HHmmss')" +Run "Stage Phase-1-Dateien in $staging" { + New-Item -ItemType Directory -Path $staging | Out-Null + New-Item -ItemType Directory -Path (Join-Path $staging 'admin_phase1') | Out-Null + New-Item -ItemType Directory -Path (Join-Path $staging 'admin_phase1\includes') | Out-Null + + # 8 PHP files at /var/www/ + $phpRoot = 'AdminInterface\www' + foreach ($f in 'admin.php','backup.php','fullbackup.php','debug.php','pm2logs.php','support_data.php','backend.php','jsoneditor.php') { + Copy-Item (Join-Path $phpRoot $f) (Join-Path $staging "admin_phase1\$f") + } + # 1 PHP file at /var/www/includes/ + Copy-Item (Join-Path $phpRoot 'includes\header.php') (Join-Path $staging 'admin_phase1\includes\header.php') + + # 2 BT scripts at /usr/local/bin/mupibox/ + Copy-Item 'scripts\bluetooth\pair_bt.sh' (Join-Path $staging 'pair_bt.sh') + Copy-Item 'scripts\bluetooth\remove_bt.sh' (Join-Path $staging 'remove_bt.sh') + + Get-ChildItem $staging -Recurse -File | Select-Object FullName, Length | Format-Table +} + +# --- 3. Backup + scp + deploy ------------------------------------------------ +$backupScript = (Join-Path $repoRoot 'scripts\dev\backup_box.sh') -replace '\\','/' +$deployScript = (Join-Path $repoRoot 'scripts\dev\deploy_box.sh') -replace '\\','/' + +Run "Backup auf der Box anlegen" { + # backup_box.sh prints the backup-dir as its last line — capture it. + # Using bash -s with stdin redirection avoids needing rsync the script onto the box. + $backupOutput = Get-Content $backupScript -Raw | & ssh $Box 'bash -s' + Write-Host $backupOutput + # backup_box.sh's last line is " /home/dietpi/mupibox-backup-" — match + # whole-line so we don't pick up "4.0K\t/home/..." from `du -sh` etc. + $script:backupDir = ($backupOutput -split "`n" | Where-Object { $_ -match '^\s*/home/dietpi/mupibox-backup-\S+\s*$' } | Select-Object -Last 1).Trim() + if (-not $script:backupDir) { + throw "Konnte Backup-Pfad nicht aus backup_box.sh-Output extrahieren" + } + Write-Host " Backup: $script:backupDir" -ForegroundColor Green +} + +Run "scp deploy.zip + admin_phase1/ + BT-Skripte → ${Box}:/tmp/" { + & scp (Join-Path $repoRoot 'src\deploy.zip') "${Box}:/tmp/deploy.zip" + & scp -r (Join-Path $staging 'admin_phase1') "${Box}:/tmp/" + & scp (Join-Path $staging 'pair_bt.sh') "${Box}:/tmp/pair_bt.sh" + & scp (Join-Path $staging 'remove_bt.sh') "${Box}:/tmp/remove_bt.sh" +} + +Run "Remote: deploy_box.sh $script:backupDir" { + Get-Content $deployScript -Raw | & ssh $Box "bash -s -- $script:backupDir" +} + +Run "Cleanup Staging $staging" { + Remove-Item $staging -Recurse -Force +} + +Write-Host "" +Write-Host "==> Phase-1-Deployment fertig." -ForegroundColor Green +Write-Host "Verifikation auf der Box (siehe Phase-1-Plan):" +Write-Host " 1. curl -i http://$($Box.Split('@')[1])/backup.php # ohne Login → Login-Form / 403" +Write-Host " 2. curl -i 'http://$($Box.Split('@')[1])/admin.php?hshutdown=1' # darf Box NICHT ausschalten" +Write-Host " 3. ssh $Box '/usr/local/bin/mupibox/pair_bt.sh \"; echo PWNED\"' # → Error: invalid MAC" +Write-Host " 4. Browser: jsoneditor.php → Form ohne csrf_token POSTen → ❌-Fehler" +Write-Host " 5. Echtes BT-Pairing mit gültiger MAC funktioniert weiter" +Write-Host "" +Write-Host "Bei Problemen Rollback auf der Box:" +Write-Host " bash restore_box.sh $script:backupDir" diff --git a/scripts/dev/restore_box.sh b/scripts/dev/restore_box.sh new file mode 100644 index 00000000..6ed7d751 --- /dev/null +++ b/scripts/dev/restore_box.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# +# Roll a MuPiBox back to a snapshot taken with backup_box.sh. Run ON THE BOX: +# +# ssh dietpi@ 'bash -s' /home/dietpi/mupibox-backup- < scripts/dev/restore_box.sh +# +# Or interactively after sshing in: +# +# bash restore_box.sh /home/dietpi/mupibox-backup- +# +# Restores code (backend-api, backend-player, www/, on-box scripts). +# User-state JSONs (resume.json, data.json, mupiboxconfig.json) are listed +# at the end as suggested commands — restoring them is OPTIONAL because the +# deploy doesn't touch them. Only restore those if a release actually broke +# the data. + +set -euo pipefail + +if [ "${#}" -lt 1 ]; then + echo "Usage: $0 /home/dietpi/mupibox-backup-" + echo + echo "Vorhandene Backups:" + ls -dt /home/dietpi/mupibox-backup-* 2>/dev/null || echo " (keine gefunden)" + exit 1 +fi + +BACKUP_DIR="${1}" + +if [ ! -d "${BACKUP_DIR}" ]; then + echo "FEHLER: ${BACKUP_DIR} existiert nicht." + exit 1 +fi + +echo "==> Quelle: ${BACKUP_DIR}" + +echo "==> Stoppe Services" +pm2 stop server spotify-control || true + +echo "==> Restore backend-api" +cp -a "${BACKUP_DIR}/server.js" /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server.js +rm -rf /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www +cp -a "${BACKUP_DIR}/www" /home/dietpi/.mupibox/Sonos-Kids-Controller-master/www + +echo "==> Restore backend-player" +cp -a "${BACKUP_DIR}/spotify-control.js" /home/dietpi/.mupibox/spotifycontroller-main/spotify-control.js + +if [ -d "${BACKUP_DIR}/admin-www" ]; then + echo "==> Restore Admin-Interface (PHP)" + sudo rsync -a --delete "${BACKUP_DIR}/admin-www/" /var/www/ +fi + +echo "==> Restore Trim-Skripte" +[ -f "${BACKUP_DIR}/remove_max_resume.sh" ] && \ + sudo install -m 755 -o root -g root "${BACKUP_DIR}/remove_max_resume.sh" /usr/local/bin/mupibox/ \ + && echo " remove_max_resume.sh wiederhergestellt" \ + || echo " (kein remove_max_resume.sh im Backup)" +[ -f "${BACKUP_DIR}/clearresume.sh" ] && \ + sudo install -m 755 -o root -g root "${BACKUP_DIR}/clearresume.sh" /usr/local/bin/mupibox/ \ + && echo " clearresume.sh wiederhergestellt" \ + || echo " (kein clearresume.sh im Backup)" + +echo "==> Starte Services" +pm2 start server spotify-control +pm2 status + +echo +echo "==> Code-Rollback fertig." +echo "==> User-State wurde NICHT zurückgespielt. Falls nötig:" +echo " cp ${BACKUP_DIR}/resume.json /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/" +echo " cp ${BACKUP_DIR}/data.json /home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/" +echo " sudo cp ${BACKUP_DIR}/mupiboxconfig.json /etc/mupibox/" diff --git a/update/conf_update.sh b/update/conf_update.sh index 9098b8da..732f14b5 100644 --- a/update/conf_update.sh +++ b/update/conf_update.sh @@ -5,328 +5,298 @@ #SRC="https://mupibox.de/version/latest" CONFIG="/etc/mupibox/mupiboxconfig.json" +# H4: Phase-3-Pattern. Every previous update step used +# `cat <<< $(jq ) > ${CONFIG}` which truncates CONFIG before jq's +# output is appended. If jq errors (or the script is killed mid-update), +# CONFIG ends up empty/corrupted — and an empty mupiboxconfig.json bricks +# the box. Wrap every write in a tmpfile + atomic mv. Same pattern that +# Phase-3 applied to 12 other scripts; conf_update was missed. +update_config() { + # Usage: update_config '' [--arg name value ...] + local filter=$1 + shift + local tmp="${CONFIG}.tmp.$$" + if /usr/bin/jq "$@" "$filter" "${CONFIG}" > "${tmp}"; then + mv "${tmp}" "${CONFIG}" + else + rm -f "${tmp}" + echo "WARN: conf_update jq filter failed: $filter" >&2 + fi +} + +# H4: previous theme-insert used `cat ${CONFIG} | grep ` to test +# whether a theme is already installed. That matches anywhere in the +# JSON file as substring — e.g. checking for "matrix" matches the literal +# "matrix" wherever it appears in the config, including inside other +# values. Use jq's array index check against the actual installedThemes +# list instead — exact-match, no false positives. +ensure_theme() { + local theme=$1 + if /usr/bin/jq -e --arg v "$theme" \ + '(.mupibox.installedThemes // []) | index($v) != null' \ + "${CONFIG}" >/dev/null 2>&1; then + return 0 # already installed + fi + update_config '.mupibox.installedThemes? += [$v]' --arg v "$theme" +} + # 1.0.8 -/usr/bin/cat <<< $(/usr/bin/jq 'del(.mupibox.googlettslanguages)' ${CONFIG}) > ${CONFIG} -/usr/bin/cat <<< $(/usr/bin/jq 'del(.mupibox.mediaCheckTimer)' ${CONFIG}) > ${CONFIG} -/usr/bin/cat <<< $(/usr/bin/jq 'del(.mupibox.AudioDevices)' ${CONFIG}) > ${CONFIG} +update_config 'del(.mupibox.googlettslanguages)' +update_config 'del(.mupibox.mediaCheckTimer)' +update_config 'del(.mupibox.AudioDevices)' # 1.0.8 -/usr/bin/cat <<< $(/usr/bin/jq '.mupibox.googlettslanguages = [{"iso639-1": "ar", "Language": "Arabic"},{"iso639-1": "zh", "Language": "Chinese"},{"iso639-1": "cs","Language": "Czech"},{"iso639-1": "da","Language": "Danish"},{"iso639-1": "nl","Language": "Dutch"},{"iso639-1": "en","Language": "English"},{"iso639-1": "fi","Language": "Finnish"},{"iso639-1": "fr","Language": "French"},{"iso639-1": "de","Language": "German"},{"iso639-1": "el","Language": "Greek"},{"iso639-1": "hi","Language": "Hindi"},{"iso639-1": "it","Language": "Italian"},{"iso639-1": "ja","Language": "Japanese"},{"iso639-1": "no","Language": "Norwegian"},{"iso639-1": "pl","Language": "Polish"},{"iso639-1": "pt","Language": "Portuguese"},{"iso639-1": "ru","Language": "Russian"},{"iso639-1": "es","Language": "Spanish, Castilian"},{"iso639-1": "sv","Language": "Swedish"},{"iso639-1": "tr","Language": "Turkish"},{"iso639-1": "uk","Language": "Ukrainian"}]' ${CONFIG}) > ${CONFIG} +update_config '.mupibox.googlettslanguages = [{"iso639-1": "ar", "Language": "Arabic"},{"iso639-1": "zh", "Language": "Chinese"},{"iso639-1": "cs","Language": "Czech"},{"iso639-1": "da","Language": "Danish"},{"iso639-1": "nl","Language": "Dutch"},{"iso639-1": "en","Language": "English"},{"iso639-1": "fi","Language": "Finnish"},{"iso639-1": "fr","Language": "French"},{"iso639-1": "de","Language": "German"},{"iso639-1": "el","Language": "Greek"},{"iso639-1": "hi","Language": "Hindi"},{"iso639-1": "it","Language": "Italian"},{"iso639-1": "ja","Language": "Japanese"},{"iso639-1": "no","Language": "Norwegian"},{"iso639-1": "pl","Language": "Polish"},{"iso639-1": "pt","Language": "Portuguese"},{"iso639-1": "ru","Language": "Russian"},{"iso639-1": "es","Language": "Spanish, Castilian"},{"iso639-1": "sv","Language": "Swedish"},{"iso639-1": "tr","Language": "Turkish"},{"iso639-1": "uk","Language": "Ukrainian"}]' # 1.0.8 DEVICE=$(/usr/bin/jq -r .spotify.physicalDevice ${CONFIG}) -if [ "$DEVICE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "hifiberry-dac" '.mupibox.physicalDevice = $v' ${CONFIG}) > ${CONFIG} +if [ "$DEVICE" == "null" ]; then + update_config '.mupibox.physicalDevice = $v' --arg v "hifiberry-dac" fi # 1.0.8 MAXVOL=$(/usr/bin/jq -r .mupibox.maxVolume ${CONFIG}) -if [ "$MAXVOL" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "100" '.mupibox.maxVolume = $v' ${CONFIG}) > ${CONFIG} +if [ "$MAXVOL" == "null" ]; then + update_config '.mupibox.maxVolume = $v' --arg v "100" fi # 2.0.0 -XMAS=$(/usr/bin/cat ${CONFIG} | grep xmas) -if [[ -z ${XMAS} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "xmas" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -IMAN=$(/usr/bin/cat ${CONFIG} | grep ironman) -if [[ -z ${IMAN} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "ironman" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -CAP=$(/usr/bin/cat ${CONFIG} | grep captainamerica) -if [[ -z ${CAP} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "captainamerica" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -WOOD=$(/usr/bin/cat ${CONFIG} | grep wood) -if [[ -z ${WOOD} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "wood" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -MATRIX=$(/usr/bin/cat ${CONFIG} | grep matrix) -if [[ -z ${MATRIX} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "matrix" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -MINT=$(/usr/bin/cat ${CONFIG} | grep mint) -if [[ -z ${MINT} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "mint" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -DANGER=$(/usr/bin/cat ${CONFIG} | grep danger) -if [[ -z ${DANGER} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "danger" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -CINEMA=$(/usr/bin/cat ${CONFIG} | grep cinema) -if [[ -z ${CINEMA} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "cinema" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi +ensure_theme xmas +ensure_theme ironman +ensure_theme captainamerica +ensure_theme wood +ensure_theme matrix +ensure_theme mint +ensure_theme danger +ensure_theme cinema #2.1.0 LEDMAX=$(/usr/bin/jq -r .shim.ledBrightnessMax ${CONFIG}) -if [ "$LEDMAX" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "100" '.shim.ledBrightnessMax = $v' ${CONFIG}) > ${CONFIG} +if [ "$LEDMAX" == "null" ]; then + update_config '.shim.ledBrightnessMax = $v' --arg v "100" fi LEDMIN=$(/usr/bin/jq -r .shim.ledBrightnessMin ${CONFIG}) -if [ "$LEDMIN" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "10" '.shim.ledBrightnessMin = $v' ${CONFIG}) > ${CONFIG} +if [ "$LEDMIN" == "null" ]; then + update_config '.shim.ledBrightnessMin = $v' --arg v "10" fi #3.0.0 PM2RAMLOG=$(/usr/bin/jq -r .pm2.ramlog ${CONFIG}) -if [ "$PM2RAMLOG" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "0" '.pm2.ramlog = $v' ${CONFIG}) > ${CONFIG} -fi - -EARTH=$(/usr/bin/cat ${CONFIG} | grep earth) -if [[ -z ${EARTH} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "earth" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} +if [ "$PM2RAMLOG" == "null" ]; then + update_config '.pm2.ramlog = $v' --arg v "0" fi -STEAMPUNK=$(/usr/bin/cat ${CONFIG} | grep steampunk) -if [[ -z ${STEAMPUNK} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "steampunk" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -FANTASY_BUTTERFLIES=$(/usr/bin/cat ${CONFIG} | grep fantasybutterflies) -if [[ -z ${FANTASY_BUTTERFLIES} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "fantasybutterflies" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -LINES=$(/usr/bin/cat ${CONFIG} | grep lines) -if [[ -z ${LINES} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "lines" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi +ensure_theme earth +ensure_theme steampunk +ensure_theme fantasybutterflies +ensure_theme lines #3.0.2 -TELEGRAM=$(/usr/bin/cat ${CONFIG} | grep telegram) +TELEGRAM=$(/usr/bin/cat ${CONFIG} | grep -E '"telegram"\s*:') if [[ -z ${TELEGRAM} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "" '.telegram.token = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.telegram.active = false' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "" '.telegram.chatId = $v' ${CONFIG}) > ${CONFIG} + update_config '.telegram.token = $v' --arg v "" + update_config '.telegram.active = false' + update_config '.telegram.chatId = $v' --arg v "" fi #3.0.2 -WLED=$(/usr/bin/cat ${CONFIG} | grep wled) +WLED=$(/usr/bin/cat ${CONFIG} | grep -E '"wled"\s*:') if [[ -z ${WLED} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq '.wled.active = false' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "" '.wled.startup_id = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "" '.wled.main_id = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "" '.wled.shutdown_id = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "255" '.wled.brightness_default = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "128" '.wled.brightness_dimmed = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "true" '.wled.boot_active = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "true" '.wled.shutdown_active = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "115200" '.wled.baud_rate = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "/dev/ttyUSB0" '.wled.com_port = $v' ${CONFIG}) > ${CONFIG} + update_config '.wled.active = false' + update_config '.wled.startup_id = $v' --arg v "" + update_config '.wled.main_id = $v' --arg v "" + update_config '.wled.shutdown_id = $v' --arg v "" + update_config '.wled.brightness_default = $v' --arg v "255" + update_config '.wled.brightness_dimmed = $v' --arg v "128" + update_config '.wled.boot_active = $v' --arg v "true" + update_config '.wled.shutdown_active = $v' --arg v "true" + update_config '.wled.baud_rate = $v' --arg v "115200" + update_config '.wled.com_port = $v' --arg v "/dev/ttyUSB0" fi #3.2.6 -IPCONTROL=$(/usr/bin/cat ${CONFIG} | grep ip_control_backend) -if [[ -z ${IPCONTROL} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "false" '.mupibox.ip_control_backend = $v' ${CONFIG}) > ${CONFIG} -fi -/usr/bin/cat <<< $(/usr/bin/jq 'del(.wled.ip)' ${CONFIG}) > ${CONFIG} -WLED=$(/usr/bin/cat ${CONFIG} | grep com_port) - -if [[ -z ${WLED} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "" '.wled.startup_id = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "255" '.wled.brightness_default = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "128" '.wled.brightness_dimmed = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "true" '.wled.boot_active = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "true" '.wled.shutdown_active = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "115200" '.wled.baud_rate = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "/dev/ttyUSB0" '.wled.com_port = $v' ${CONFIG}) > ${CONFIG} - +IPCONTROL=$(/usr/bin/jq -r '.mupibox.ip_control_backend' ${CONFIG}) +if [ "$IPCONTROL" == "null" ]; then + update_config '.mupibox.ip_control_backend = $v' --arg v "false" +fi +update_config 'del(.wled.ip)' +WLED_COM=$(/usr/bin/jq -r '.wled.com_port' ${CONFIG}) +if [ "$WLED_COM" == "null" ]; then + update_config '.wled.startup_id = $v' --arg v "" + update_config '.wled.brightness_default = $v' --arg v "255" + update_config '.wled.brightness_dimmed = $v' --arg v "128" + update_config '.wled.boot_active = $v' --arg v "true" + update_config '.wled.shutdown_active = $v' --arg v "true" + update_config '.wled.baud_rate = $v' --arg v "115200" + update_config '.wled.com_port = $v' --arg v "/dev/ttyUSB0" fi #3.3.4 GPU=$(/usr/bin/jq -r .chromium.gpu ${CONFIG}) -if [ "$GPU" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.chromium.gpu = false' ${CONFIG}) > ${CONFIG} +if [ "$GPU" == "null" ]; then + update_config '.chromium.gpu = false' fi SCROLLANI=$(/usr/bin/jq -r .chromium.sccrollanimation ${CONFIG}) -if [ "$SCROLLANI" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.chromium.sccrollanimation = false' ${CONFIG}) > ${CONFIG} +if [ "$SCROLLANI" == "null" ]; then + update_config '.chromium.sccrollanimation = false' fi CACHEPATH=$(/usr/bin/jq -r .chromium.cachepath ${CONFIG}) -if [ "$CACHEPATH" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "/home/dietpi/.mupibox/chromium_cache" '.chromium.cachepath = $v' ${CONFIG}) > ${CONFIG} +if [ "$CACHEPATH" == "null" ]; then + update_config '.chromium.cachepath = $v' --arg v "/home/dietpi/.mupibox/chromium_cache" fi CACHESIZE=$(/usr/bin/jq -r .chromium.cachesize ${CONFIG}) -if [ "$CACHESIZE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "128" '.chromium.cachesize = $v' ${CONFIG}) > ${CONFIG} +if [ "$CACHESIZE" == "null" ]; then + update_config '.chromium.cachesize = $v' --arg v "128" fi KIOSKMODE=$(/usr/bin/jq -r .chromium.kiosk ${CONFIG}) -if [ "$KIOSKMODE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.chromium.kiosk = true' ${CONFIG}) > ${CONFIG} +if [ "$KIOSKMODE" == "null" ]; then + update_config '.chromium.kiosk = true' fi MAXCACHE=$(/usr/bin/jq -r .spotify.maxcachesize ${CONFIG}) -if [ "$MAXCACHE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "1073741824" '.spotify.maxcachesize = $v' ${CONFIG}) > ${CONFIG} +if [ "$MAXCACHE" == "null" ]; then + update_config '.spotify.maxcachesize = $v' --arg v "1073741824" fi CACHEPATH=$(/usr/bin/jq -r .spotify.cachepath ${CONFIG}) -if [ "$CACHEPATH" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "/home/dietpi/.cache/spotifyd" '.spotify.cachepath = $v' ${CONFIG}) > ${CONFIG} +if [ "$CACHEPATH" == "null" ]; then + update_config '.spotify.cachepath = $v' --arg v "/home/dietpi/.cache/spotifyd" fi CACHESTATE=$(/usr/bin/jq -r .spotify.cachestate ${CONFIG}) -if [ "$CACHESTATE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.spotify.cachestate = true' ${CONFIG}) > ${CONFIG} +if [ "$CACHESTATE" == "null" ]; then + update_config '.spotify.cachestate = true' fi MQTTDEBUG=$(/usr/bin/jq -r .mqtt.debug ${CONFIG}) -if [ "$MQTTDEBUG" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.mqtt.debug = false' ${CONFIG}) > ${CONFIG} +if [ "$MQTTDEBUG" == "null" ]; then + update_config '.mqtt.debug = false' fi MQTTACTIVE=$(/usr/bin/jq -r .mqtt.active ${CONFIG}) -if [ "$MQTTACTIVE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.mqtt.active = false' ${CONFIG}) > ${CONFIG} +if [ "$MQTTACTIVE" == "null" ]; then + update_config '.mqtt.active = false' fi MQTTBROKER=$(/usr/bin/jq -r .mqtt.broker ${CONFIG}) -if [ "$MQTTBROKER" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "mqtt-example-broker.com" '.mqtt.broker = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTBROKER" == "null" ]; then + update_config '.mqtt.broker = $v' --arg v "mqtt-example-broker.com" fi MQTTPORT=$(/usr/bin/jq -r .mqtt.port ${CONFIG}) -if [ "$MQTTPORT" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "1883" '.mqtt.port = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTPORT" == "null" ]; then + update_config '.mqtt.port = $v' --arg v "1883" fi MQTTTOPIC=$(/usr/bin/jq -r .mqtt.topic ${CONFIG}) -if [ "$MQTTTOPIC" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "MuPiBox/Boxname" '.mqtt.topic = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTTOPIC" == "null" ]; then + update_config '.mqtt.topic = $v' --arg v "MuPiBox/Boxname" fi MQTTBOXNAME=$(/usr/bin/jq -r .mqtt.clientId ${CONFIG}) -if [ "$MQTTBOXNAME" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "Boxname" '.mqtt.clientId = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTBOXNAME" == "null" ]; then + update_config '.mqtt.clientId = $v' --arg v "Boxname" fi MQTTUSERNAME=$(/usr/bin/jq -r .mqtt.username ${CONFIG}) -if [ "$MQTTUSERNAME" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "username" '.mqtt.username = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTUSERNAME" == "null" ]; then + update_config '.mqtt.username = $v' --arg v "username" fi MQTTPASSWORD=$(/usr/bin/jq -r .mqtt.password ${CONFIG}) -if [ "$MQTTPASSWORD" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "password" '.mqtt.password = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTPASSWORD" == "null" ]; then + update_config '.mqtt.password = $v' --arg v "password" fi MQTTREFRESH=$(/usr/bin/jq -r .mqtt.refresh ${CONFIG}) -if [ "$MQTTREFRESH" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "5" '.mqtt.refresh = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTREFRESH" == "null" ]; then + update_config '.mqtt.refresh = $v' --arg v "5" fi MQTTREFRESHIDLE=$(/usr/bin/jq -r .mqtt.refreshIdle ${CONFIG}) -if [ "$MQTTREFRESHIDLE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "30" '.mqtt.refreshIdle = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTREFRESHIDLE" == "null" ]; then + update_config '.mqtt.refreshIdle = $v' --arg v "30" fi MQTTTIMEOUT=$(/usr/bin/jq -r .mqtt.timeout ${CONFIG}) -if [ "$MQTTTIMEOUT" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "60" '.mqtt.timeout = $v' ${CONFIG}) > ${CONFIG} +if [ "$MQTTTIMEOUT" == "null" ]; then + update_config '.mqtt.timeout = $v' --arg v "60" fi HA_MQTT=$(/usr/bin/jq -r .mqtt.ha_topic ${CONFIG}) -if [ "$HA_MQTT" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.mqtt.ha_active = false' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "homeassistant" '.mqtt.ha_topic = $v' ${CONFIG}) > ${CONFIG} +if [ "$HA_MQTT" == "null" ]; then + update_config '.mqtt.ha_active = false' + update_config '.mqtt.ha_topic = $v' --arg v "homeassistant" fi +# H4: the original mupihat-init blob had a malformed jq filter — a +# trailing string literal `"ENERpower 2S2P 10.000mAh"` inside the object +# that produced a jq parse error. It only fired when selected_battery +# was missing on a fresh box, so existing boxes weren't affected, but +# any new install would trip it and (with the old truncate-race) could +# leave CONFIG empty. Rewrite as a clean nested assign. BATTERYCONFIG=$(/usr/bin/jq -r .mupihat.selected_battery ${CONFIG}) -if [ "$BATTERYCONFIG" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "ENERpower 2S2P 10.000mAh" '.mupihat.selected_battery = $v' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '. += {"mupihat": { "battery_types": [{ "name": "Ansmann 2S1P", "config": { "v_100": "8100", "v_75": "7800", "v_50": "7400", "v_25": "7000", "v_0": "6700", "th_warning": "7000", "th_shutdown": "6800" }}, { "name": "ENERpower 2S2P 10.000mAh", "config": { "v_100": "8000", "v_75": "7700", "v_50": "7300", "v_25": "6900", "v_0": "6000", "th_warning": "6500", "th_shutdown": "6150" }}, { "name": "USB-C mode (no battery)", "config": { "v_100": "1", "v_75": "1", "v_50": "1", "v_25": "1", "v_0": "1", "th_warning": "0", "th_shutdown": "0"}}, { "name": "Custom", "config": { "v_100": "8100", "v_75": "7800", "v_50": "7400", "v_25": "7000", "v_0": "6700", "th_warning": "7000", "th_shutdown": "6800"}}], "ENERpower 2S2P 10.000mAh" }}' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.mupihat.hat_active = false' ${CONFIG}) > ${CONFIG} -fi - -LINES=$(/usr/bin/cat ${CONFIG} | grep lines) -if [[ -z ${LINES} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "lines" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi +if [ "$BATTERYCONFIG" == "null" ]; then + update_config '.mupihat.selected_battery = $v' --arg v "ENERpower 2S2P 10.000mAh" + update_config '.mupihat.battery_types = [ + { "name": "Ansmann 2S1P", "config": { "v_100": "8100", "v_75": "7800", "v_50": "7400", "v_25": "7000", "v_0": "6700", "th_warning": "7000", "th_shutdown": "6800" }}, + { "name": "ENERpower 2S2P 10.000mAh", "config": { "v_100": "8200", "v_75": "7700", "v_50": "7400", "v_25": "7000", "v_0": "6400", "th_warning": "6700", "th_shutdown": "6500" }}, + { "name": "ENERpower 2S3P 15.000mAh", "config": { "v_100": "8200", "v_75": "7700", "v_50": "7400", "v_25": "7000", "v_0": "6400", "th_warning": "6700", "th_shutdown": "6500" }}, + { "name": "USB-C mode (no battery)", "config": { "v_100": "1", "v_75": "1", "v_50": "1", "v_25": "1", "v_0": "1", "th_warning": "0", "th_shutdown": "0" }}, + { "name": "Custom", "config": { "v_100": "8100", "v_75": "7800", "v_50": "7400", "v_25": "7000", "v_0": "6700", "th_warning": "7000", "th_shutdown": "6800" }} + ]' + update_config '.mupihat.hat_active = false' +fi + +# Idempotent backfill: appends the 2S3P profile to existing boxes that already +# have a battery_types array (pre-2S3P installs). Touches neither selected_battery +# nor any other entry — user's current profile choice + custom values are preserved. +HAS_2S3P=$(/usr/bin/jq -r '[.mupihat.battery_types[]?.name] | index("ENERpower 2S3P 15.000mAh")' ${CONFIG}) +if [ "$HAS_2S3P" == "null" ]; then + update_config '.mupihat.battery_types += [{ + "name": "ENERpower 2S3P 15.000mAh", + "config": { "v_100": "8200", "v_75": "7700", "v_50": "7400", "v_25": "7000", "v_0": "6400", "th_warning": "6700", "th_shutdown": "6500" } + }]' +fi + +ensure_theme lines HAT_ACTIVE=$(/usr/bin/jq -r .mupihat.hat_active ${CONFIG}) -if [ "$HAT_ACTIVE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.mupihat.hat_active = false' ${CONFIG}) > ${CONFIG} +if [ "$HAT_ACTIVE" == "null" ]; then + update_config '.mupihat.hat_active = false' fi FAN_ACTIVE=$(/usr/bin/jq -r .fan.fan_active ${CONFIG}) -if [ "$FAN_ACTIVE" == "null" ]; then - /usr/bin/cat <<< $(/usr/bin/jq '.fan.fan_active = false' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.fan.fan_gpio = "13"' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.fan.fan_temp_100 = "75"' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.fan.fan_temp_75 = "65"' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.fan.fan_temp_50 = "55"' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.fan.fan_temp_25 = "45"' ${CONFIG}) > ${CONFIG} +if [ "$FAN_ACTIVE" == "null" ]; then + update_config '.fan.fan_active = false' + update_config '.fan.fan_gpio = "13"' + update_config '.fan.fan_temp_100 = "75"' + update_config '.fan.fan_temp_75 = "65"' + update_config '.fan.fan_temp_50 = "55"' + update_config '.fan.fan_temp_25 = "45"' fi -FORMS=$(/usr/bin/cat ${CONFIG} | grep forms) -if [[ -z ${FORMS} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "forms" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -COMIC=$(/usr/bin/cat ${CONFIG} | grep comic) -if [[ -z ${COMIC} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "comic" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi +ensure_theme forms +ensure_theme comic +ensure_theme mystic -MYSTIC=$(/usr/bin/cat ${CONFIG} | grep mystic) -if [[ -z ${MYSTIC} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "mystic" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} +RESUME=$(/usr/bin/jq -r '.mupibox.resume' ${CONFIG}) +if [ "$RESUME" == "null" ]; then + update_config '.mupibox.resume = 9' fi -RESUME=$(/usr/bin/cat ${CONFIG} | grep resume) -if [[ -z ${RESUME} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq '.mupibox.resume = 9' ${CONFIG}) > ${CONFIG} -fi +ensure_theme clone-wars +ensure_theme enterprise +ensure_theme spiderman +ensure_theme pikachu +ensure_theme supermario +ensure_theme dinosaur +ensure_theme unicorn +ensure_theme axolotl -CLONE=$(/usr/bin/cat ${CONFIG} | grep clone-wars) -if [[ -z ${CLONE} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "clone-wars" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} +CUSTOMTHEME=$(/usr/bin/jq -r '.mupibox.customTheme' ${CONFIG}) +if [ "$CUSTOMTHEME" == "null" ]; then + ensure_theme custom + update_config '.mupibox.customTheme = ""' fi -ENTERPRISE=$(/usr/bin/cat ${CONFIG} | grep enterprise) -if [[ -z ${ENTERPRISE} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "enterprise" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} +ADMININTERFACE=$(/usr/bin/jq -r '.interfacelogin.state' ${CONFIG}) +if [ "$ADMININTERFACE" == "null" ]; then + update_config '.interfacelogin.state = false' + update_config '.interfacelogin.password = $v' --arg v '$2y$10$tA27/5vXFUPgjfjfi7dpTuk.1yOffsg6kuSDQBGTv4sjpVkRlhd76' fi -SPIDERMAN=$(/usr/bin/cat ${CONFIG} | grep spiderman) -if [[ -z ${SPIDERMAN} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "spiderman" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi -PIKACHU=$(/usr/bin/cat ${CONFIG} | grep pikachu) -if [[ -z ${PIKACHU} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "pikachu" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -SUPERMARIO=$(/usr/bin/cat ${CONFIG} | grep supermario) -if [[ -z ${SUPERMARIO} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "supermario" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -DINO=$(/usr/bin/cat ${CONFIG} | grep dinosaur) -if [[ -z ${DINO} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "dinosaur" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -UNICORN=$(/usr/bin/cat ${CONFIG} | grep unicorn) -if [[ -z ${UNICORN} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "unicorn" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -AXOLOTL=$(/usr/bin/cat ${CONFIG} | grep axolotl) -if [[ -z ${AXOLOTL} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "axolotl" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} -fi - -CUSTOMTHEME=$(/usr/bin/cat ${CONFIG} | grep customTheme) -if [[ -z ${CUSTOMTHEME} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq --arg v "custom" '.mupibox.installedThemes? += [$v]' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq '.mupibox.customTheme = ""' ${CONFIG}) > ${CONFIG} -fi - -ADMININTERFACE=$(/usr/bin/cat ${CONFIG} | grep interfacelogin) -if [[ -z ${ADMININTERFACE} ]]; then - /usr/bin/cat <<< $(/usr/bin/jq '.interfacelogin.state = false' ${CONFIG}) > ${CONFIG} - /usr/bin/cat <<< $(/usr/bin/jq --arg v "$2y$10$tA27/5vXFUPgjfjfi7dpTuk.1yOffsg6kuSDQBGTv4sjpVkRlhd76" '.interfacelogin.password = $v' ${CONFIG}) > ${CONFIG} -fi - - -#/usr/bin/cat <<< $(/usr/bin/jq '.mupibox.AudioDevices += [{"tname": "MAX98357A bcm2835-i2s-HiFi HiFi-0","ufname": "MAX98357A bcm2835-i2s-HiFi HiFi-0"},{"tname": "rpi-bcm2835-3.5mm","ufname": "Onboard 3.5mm output"},{"tname": "rpi-bcm2835-hdmi","ufname": "Onboard HDMI output"},{"tname": "hifiberry-amp","ufname": "HifiBerry AMP / AMP+"},{"tname": "hifiberry-dac","ufname": "HifiBerry DAC / MiniAmp"},{"tname": "hifiberry-dacplus","ufname": "HifiBerry DAC+ / DAC+ Pro / AMP2"},{"tname": "usb-dac","ufname": "Any USB Audio DAC (Auto detection)"}]' ${CONFIG}) > ${CONFIG} -/usr/bin/cat <<< $(/usr/bin/jq '.mupibox.AudioDevices = [{"tname": "MAX98357A bcm2835-i2s-HiFi HiFi-0","ufname": "MAX98357A bcm2835-i2s-HiFi HiFi-0"},{"tname": "rpi-bcm2835-3.5mm","ufname": "Onboard 3.5mm output"},{"tname": "rpi-bcm2835-hdmi","ufname": "Onboard HDMI output"},{"tname": "allo-boss-dac-pcm512x-audio","ufname": "Allo Boss DAC"},{"tname": "allo-boss2-dac-audio","ufname": "Allo Boss2 DAC"},{"tname": "allo-digione","ufname": "Allo DigiOne"},{"tname": "allo-katana-dac-audio","ufname": "Allo Katana DAC"},{"tname": "allo-piano-dac-pcm512x-audio","ufname": "Allo Piano DAC"},{"tname": "allo-piano-dac-plus-pcm512x-audio","ufname": "Allo Piano DAC 2.1"},{"tname": "applepi-dac","ufname": "ApplePi DAC (Orchard Audio)"},{"tname": "dionaudio-loco","ufname": "Dion Audio LOCO"},{"tname": "dionaudio-loco-v2","ufname": "Dion Audio LOCO V2"},{"tname": "googlevoicehat-soundcard","ufname": "Google AIY voice kit"},{"tname": "hifiberry-amp","ufname": "HifiBerry AMP / AMP+"},{"tname": "hifiberry-dac","ufname": "HifiBerry DAC / MiniAmp"},{"tname": "hifiberry-dacplus","ufname": "HifiBerry DAC+ / DAC+ Pro / AMP2"},{"tname": "hifiberry-dacplusadc","ufname": "HifiBerry DAC+ADC"},{"tname": "hifiberry-dacplusadcpro","ufname": "HifiBerry DAC+ADC Pro"},{"tname": "hifiberry-dacplusdsp","ufname": "HifiBerry DAC+DSP"},{"tname": "hifiberry-dacplushd","ufname": "HifiBerry DAC+ HD"},{"tname": "hifiberry-digi","ufname": "HifiBerry Digi / Digi+"},{"tname": "hifiberry-digi-pro","ufname": "HifiBerry Digi+ Pro"},{"tname": "i-sabre-q2m","ufname": "AudioPhonics I-Sabre ES9028Q2M / ES9038Q2M"},{"tname": "iqaudio-codec","ufname": "IQaudIO Pi-Codec HAT"},{"tname": "iqaudio-dac","ufname": "IQaudIO DAC audio card"},{"tname": "iqaudio-dacplus","ufname": "Pi-DAC+, Pi-DACZero, Pi-DAC+ Pro, Pi-DigiAMP+"},{"tname": "iqaudio-digi-wm8804-audio","ufname": "Pi-Digi+"},{"tname": "usb-dac","ufname": "Any USB Audio DAC (Auto detection)"}]' ${CONFIG}) > ${CONFIG} +update_config '.mupibox.AudioDevices = [{"tname": "MAX98357A bcm2835-i2s-HiFi HiFi-0","ufname": "MAX98357A bcm2835-i2s-HiFi HiFi-0"},{"tname": "rpi-bcm2835-3.5mm","ufname": "Onboard 3.5mm output"},{"tname": "rpi-bcm2835-hdmi","ufname": "Onboard HDMI output"},{"tname": "allo-boss-dac-pcm512x-audio","ufname": "Allo Boss DAC"},{"tname": "allo-boss2-dac-audio","ufname": "Allo Boss2 DAC"},{"tname": "allo-digione","ufname": "Allo DigiOne"},{"tname": "allo-katana-dac-audio","ufname": "Allo Katana DAC"},{"tname": "allo-piano-dac-pcm512x-audio","ufname": "Allo Piano DAC"},{"tname": "allo-piano-dac-plus-pcm512x-audio","ufname": "Allo Piano DAC 2.1"},{"tname": "applepi-dac","ufname": "ApplePi DAC (Orchard Audio)"},{"tname": "dionaudio-loco","ufname": "Dion Audio LOCO"},{"tname": "dionaudio-loco-v2","ufname": "Dion Audio LOCO V2"},{"tname": "googlevoicehat-soundcard","ufname": "Google AIY voice kit"},{"tname": "hifiberry-amp","ufname": "HifiBerry AMP / AMP+"},{"tname": "hifiberry-dac","ufname": "HifiBerry DAC / MiniAmp"},{"tname": "hifiberry-dacplus","ufname": "HifiBerry DAC+ / DAC+ Pro / AMP2"},{"tname": "hifiberry-dacplusadc","ufname": "HifiBerry DAC+ADC"},{"tname": "hifiberry-dacplusadcpro","ufname": "HifiBerry DAC+ADC Pro"},{"tname": "hifiberry-dacplusdsp","ufname": "HifiBerry DAC+DSP"},{"tname": "hifiberry-dacplushd","ufname": "HifiBerry DAC+ HD"},{"tname": "hifiberry-digi","ufname": "HifiBerry Digi / Digi+"},{"tname": "hifiberry-digi-pro","ufname": "HifiBerry Digi+ Pro"},{"tname": "i-sabre-q2m","ufname": "AudioPhonics I-Sabre ES9028Q2M / ES9038Q2M"},{"tname": "iqaudio-codec","ufname": "IQaudIO Pi-Codec HAT"},{"tname": "iqaudio-dac","ufname": "IQaudIO DAC audio card"},{"tname": "iqaudio-dacplus","ufname": "Pi-DAC+, Pi-DACZero, Pi-DAC+ Pro, Pi-DigiAMP+"},{"tname": "iqaudio-digi-wm8804-audio","ufname": "Pi-Digi+"},{"tname": "usb-dac","ufname": "Any USB Audio DAC (Auto detection)"}]' # delete old entries -/usr/bin/jq 'del(.spotify.username)' ${CONFIG} > /tmp/tmp.$$.json && mv /tmp/tmp.$$.json ${CONFIG} -/usr/bin/jq 'del(.spotify.password)' ${CONFIG} > /tmp/tmp.$$.json && mv /tmp/tmp.$$.json ${CONFIG} - +update_config 'del(.spotify.username)' +update_config 'del(.spotify.password)'