From caab08265dabfcb6f7e44209f344c74a297daf36 Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Thu, 7 May 2026 12:25:20 +0200 Subject: [PATCH 1/7] add backup_box.sh / restore_box.sh for safer deploys Pre-deploy snapshot of every file the deploy will replace plus the user- state JSONs (resume.json, data.json, mupiboxconfig.json), written to a timestamped /home/dietpi/mupibox-backup-/ on the SD card so a full rollback is possible. restore_box.sh takes that path and rolls back code + trim scripts; user-state restore is opt-in (printed as hints, not run) because the deploy doesn't touch those files in normal operation. Both scripts run on the box itself; from the dev machine pipe via stdin: ssh dietpi@ 'bash -s' < scripts/dev/backup_box.sh --- scripts/dev/backup_box.sh | 57 ++++++++++++++++++++++++++++++++ scripts/dev/restore_box.sh | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 scripts/dev/backup_box.sh create mode 100644 scripts/dev/restore_box.sh diff --git a/scripts/dev/backup_box.sh b/scripts/dev/backup_box.sh new file mode 100644 index 00000000..c8c9fe32 --- /dev/null +++ b/scripts/dev/backup_box.sh @@ -0,0 +1,57 @@ +#!/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 "--> 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/restore_box.sh b/scripts/dev/restore_box.sh new file mode 100644 index 00000000..f6aa637f --- /dev/null +++ b/scripts/dev/restore_box.sh @@ -0,0 +1,66 @@ +#!/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 + +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/" From dfaf56ad47caa39041956649c93c2a6034f1f592 Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Thu, 7 May 2026 12:33:46 +0200 Subject: [PATCH 2/7] add deploy_box.sh for atomic-ish on-box deploys Companion to backup_box.sh / restore_box.sh. Takes a backup directory as input, verifies the deploy.zip is well-formed before touching live files, restores the user-customised www/ files (active_theme.css, cover/, theme-data/) from the backup INTO the new www/ before swapping it in, and restarts pm2. Also installs updated trim scripts from /tmp/ if the dev machine uploaded them. Cleans its staging dir on EXIT trap. Pre-flight: scp deploy.zip + (optionally) the trim scripts to /tmp/ on the box, then pipe this script in: cmd /c "ssh dietpi@ bash -s -- < scripts\dev\deploy_box.sh" --- scripts/dev/deploy_box.sh | 117 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 scripts/dev/deploy_box.sh diff --git a/scripts/dev/deploy_box.sh b/scripts/dev/deploy_box.sh new file mode 100644 index 00000000..c2ef8c9c --- /dev/null +++ b/scripts/dev/deploy_box.sh @@ -0,0 +1,117 @@ +#!/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}" +unzip -q "${DEPLOY_ZIP}" -d "${WORK_DIR}" + +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 im Deploy-ZIP — Build kaputt?" + 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 "==> 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}" From f236cfd89c35582987caa364ab867feeb7e18fa0 Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Thu, 7 May 2026 12:39:25 +0200 Subject: [PATCH 3/7] deploy_box.sh: tolerate unzip warning exit (backslash separators) PowerShell's Compress-Archive produces ZIPs with backslash path separators, which unzip flags with exit code 1 ("warning, processing continued"). With set -e that aborted the deploy before any files were copied. Treat exit codes 0 and 1 as success; 2+ are actual failures. The post-unzip path-existence check still runs, so a genuinely broken extraction is caught before pm2 stops. --- scripts/dev/deploy_box.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/dev/deploy_box.sh b/scripts/dev/deploy_box.sh index c2ef8c9c..5f58cad0 100644 --- a/scripts/dev/deploy_box.sh +++ b/scripts/dev/deploy_box.sh @@ -55,11 +55,24 @@ 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 im Deploy-ZIP — Build kaputt?" + 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 From 992771839015967fe689fd1f7b6e1acae13b18dd Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Thu, 7 May 2026 14:17:10 +0200 Subject: [PATCH 4/7] deploy_box.sh: install telegram_*.py from /tmp + restart mupi_telegram Companion to the multi-chat Telegram refactor. The on-box telegram scripts and the new shared telegram_chats.py helper need to land in /usr/local/bin/mupibox/, and mupi_telegram.service has to restart so the receiver picks up the new ALLOWED_CHAT_IDS set and the /limit command. Pre-flight: scp scripts/telegram/*.py to /tmp/. --- scripts/dev/deploy_box.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/dev/deploy_box.sh b/scripts/dev/deploy_box.sh index 5f58cad0..c8173c7f 100644 --- a/scripts/dev/deploy_box.sh +++ b/scripts/dev/deploy_box.sh @@ -112,6 +112,25 @@ for s in remove_max_resume.sh clearresume.sh; do 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 From 3d446cd5d3395df557ecfc34a92dac1e0c1b239b Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Thu, 7 May 2026 14:38:21 +0200 Subject: [PATCH 5/7] dev scripts: cover the AdminInterface PHP files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backup_box.sh now copies /var/www to admin-www/ in the snapshot. deploy_box.sh installs smart.php (and any other PHP that lands in /tmp via scp) to /var/www, owned by dietpi:dietpi mode 644. restore_box.sh syncs admin-www/ back to /var/www on rollback. Discovered while deploying the multi-chat Telegram changes — smart.php lives at /var/www/smart.php on the box, served by Apache, not part of the Angular deploy.zip. --- scripts/dev/backup_box.sh | 4 ++++ scripts/dev/deploy_box.sh | 8 ++++++++ scripts/dev/restore_box.sh | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/scripts/dev/backup_box.sh b/scripts/dev/backup_box.sh index c8c9fe32..7dd36746 100644 --- a/scripts/dev/backup_box.sh +++ b/scripts/dev/backup_box.sh @@ -26,6 +26,10 @@ 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)" diff --git a/scripts/dev/deploy_box.sh b/scripts/dev/deploy_box.sh index c8173c7f..b54fe2aa 100644 --- a/scripts/dev/deploy_box.sh +++ b/scripts/dev/deploy_box.sh @@ -112,6 +112,14 @@ for s in remove_max_resume.sh clearresume.sh; do 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 + echo "==> Telegram-Skripte (falls in ${TRIM_SCRIPTS_DIR}/telegram_*.py)" shopt -s nullglob telegram_files=("${TRIM_SCRIPTS_DIR}"/telegram_*.py) diff --git a/scripts/dev/restore_box.sh b/scripts/dev/restore_box.sh index f6aa637f..6ed7d751 100644 --- a/scripts/dev/restore_box.sh +++ b/scripts/dev/restore_box.sh @@ -44,6 +44,11 @@ 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/ \ From f0bbde74218acfc26fafd2152a713c56bdee497f Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Thu, 7 May 2026 18:07:48 +0200 Subject: [PATCH 6/7] dev scripts: Phase-1-Deployment-Wrapper + erweitere deploy_box.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/dev/deploy_phase1.ps1 (NEU): one-shot Wrapper für Windows. Baut deploy.zip neu (frontend-box → backend-api → backend-player), stagt die 8 PHP-Phase-1-Dateien + 2 BT-Skripte, scp't alles nach /tmp/, ruft backup_box.sh + deploy_box.sh per ssh auf, druckt zum Schluss die fünf Verifikations-Curls. -SkipBuild und -DryRun für Re-Runs. - scripts/dev/deploy_box.sh: zwei neue Install-Blöcke. Wenn /tmp/admin_phase1/ existiert, werden admin.php / backup.php / fullbackup.php / debug.php / pm2logs.php / support_data.php / backend.php / jsoneditor.php nach /var/www/ und includes/header.php nach /var/www/includes/ installiert (allowlisted, kein wildcard rm). Wenn /tmp/pair_bt.sh oder /tmp/remove_bt.sh existieren, gehen die nach /usr/local/bin/mupibox/ (Mode 755, root:root). --- scripts/dev/deploy_box.sh | 28 +++++++ scripts/dev/deploy_phase1.ps1 | 146 ++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 scripts/dev/deploy_phase1.ps1 diff --git a/scripts/dev/deploy_box.sh b/scripts/dev/deploy_box.sh index b54fe2aa..f82f1461 100644 --- a/scripts/dev/deploy_box.sh +++ b/scripts/dev/deploy_box.sh @@ -120,6 +120,34 @@ 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) 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" From fed8f1d2a9f357e136c36bb8be7a3dbd47b5200e Mon Sep 17 00:00:00 2001 From: Waldemar Berg Date: Fri, 15 May 2026 15:36:49 +0200 Subject: [PATCH 7/7] feat(hat): add ENERpower 2S3P 15.000mAh profile + correct 2S2P curve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New battery profile entry for the ENERpower 2S3P 15.000mAh pack (Samsung INR21700-50E cells, 3P × 5000 mAh, charge end 8.4V, BMS cutoff 6V): v_100=8200 v_75=7700 v_50=7400 v_25=7000 v_0=6400 th_warning=6700 th_shutdown=6500 The 2P/3P distinction does not affect pack voltage — only capacity — so the curve mirrors what the corrected 2S2P entry now uses. Corrections to the existing "ENERpower 2S2P 10.000mAh" profile (Backlog L3): the old values reported 100% at 8.0V (actual top of charge is 8.4V, so display saturated below real full) and dropped 0% at exactly the BMS cutoff (6.0V, where the pack has already shut down — display never reached 0 cleanly). Same corrected curve applied. Deployment touches three places: config/templates/mupiboxconfig.json - 2S2P entry rewritten to corrected values - new 2S3P entry inserted between 2S2P and USB-C update/conf_update.sh - fresh-install jq filter (only runs when selected_battery is null) updated to include the corrected 2S2P + new 2S3P entries - new idempotent backfill block (H4-pattern): if the battery_types array does not contain "ENERpower 2S3P 15.000mAh", append it. Does not touch selected_battery or any other entry — existing profile choices and custom values are preserved. User still needs to select the new profile via Admin → MuPiHAT → Battery selection after the config migration runs. --- config/templates/mupiboxconfig.json | 24 +- update/conf_update.sh | 420 +++++++++++++--------------- 2 files changed, 213 insertions(+), 231 deletions(-) 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/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)'