Skip to content

Instantly share code, notes, and snippets.

@ddlsmurf
Last active November 4, 2025 22:34
Show Gist options
  • Save ddlsmurf/05ee450043a2a3df29f798acdb93a0c8 to your computer and use it in GitHub Desktop.
Save ddlsmurf/05ee450043a2a3df29f798acdb93a0c8 to your computer and use it in GitHub Desktop.
piCorePlayer rudimentary MQTT control. Install `bash` extension, edit the variables, copy over by scp and set as user command
#!/usr/bin/env bash
set -euo pipefail
MQTT_HOST="192.168.1.1"
MQTT_USER="username"
MQTT_PASS="hunter2"
MQTT_CLIENT_ID_PREFIX="pCP-"
MQTT_PREFIX="piCorePlayer/instance1"
MQTT_TOPIC_STATUS_PREFIX="$MQTT_PREFIX/status"
MQTT_TOPIC_COMMAND="$MQTT_PREFIX/command"
MQTT_TOPIC_MODE="$MQTT_TOPIC_STATUS_PREFIX/mode"
MQTT_TOPIC_WILL="$MQTT_TOPIC_STATUS_PREFIX/LWT"
MQTT_WILL_ONLINE="Online"
MQTT_WILL_OFFLINE="Offline"
MQTT_WILL_INIT="Starting..."
POLL_MODE_PERIOD_S=2
RETRY_SUBSCRIBE_AFTER_S=10
# I need both, without this, if the first command after power on is 'pause',
# then 'pause' commands after won't stop it again until the 'stop' command is used
PAUSE_AT_STARTUP=true # note the pause command starts playing (with the 'pause' command)
STOP_AT_STARTUP=true # if this is true also, the playing is very brief
# Because there is currently no way to read the volume, if it is hardcoded here
# the brief pause-stop at startup will be silent then set to this
VOLUME_AT_STARTUP=90
ALLOWED_PCP_COMMANDS=",play,pause,stop,up,down,next,prev,rand,power,volume,mode,sd,rb,"
# - pcp play : play current track in playlist
# - pcp stop : stop current track
# - pcp pause : pause current track
# - pcp up : volume up
# - pcp down : volume down
# - pcp next : next track
# - pcp prev : previous track
# - pcp rand : generate random playlist
# - pcp power [on|off] : software power on or off
# - pcp volume [0-100] : set volume between 0 to 100
# - pcp rescan : look for new and changed media files in connected LMS library
# - pcp wipecache : clear connected LMS library and rescan
# - pcp mode : display Squeezelite's current mode
# - pcp bu : (b)ack(u)p
# - pcp sd : (s)hut(d)own
# - pcp bs : (b)ackup then (s)hutdown
# - pcp rb : (r)e(b)oot
# - pcp br : (b)ackup then (r)eboot
# - pcp leds [on|off] : turn leds on or off
# (incomplete: https://docs.picoreplayer.org/information/pcp_cli/ )
info() { echo "$@" >&2 ; }
die() { info "$@" ; exit 1; }
run_at_exit() {
if [[ -z "${CLEANUP_FUNC_LIST:-}" ]]; then
CLEANUP_FUNC_LIST="$@"
__cleanup() { for cmd in $CLEANUP_FUNC_LIST ; do $cmd || echo "cleanup '$cmd' failed with $?" ; done ; }
trap "__cleanup" EXIT INT TERM
else
CLEANUP_FUNC_LIST="$@ $CLEANUP_FUNC_LIST"
fi
}
# mock pcp for local testing
if ! which pcp > /dev/null ; then
info "⚠️ pcp not found, using a mock"
pcp() {
case "$1" in
mode)
if [[ $SECONDS -lt 10 ]]; then
info Yikes too early
return 1
fi
local idx="$(((SECONDS / 5) % 3))"
local mode="stop"
if [[ "$idx" == 0 ]]; then
mode="pause"
elif [[ "$idx" == 1 ]]; then
mode="play"
fi
# info "🖲️ Execute pcp $1 - will be '$mode'"
echo "$mode"
;;
play|pause|stop|volume)
info "!! Execute pcp $@"
;;
*)
info "Error: unknown pcp command: '$@'"
return 1
;;
esac
}
fi
publish() {
local topic="$1"
shift
info "=> Publish '$1' to '$topic'"
mosquitto_pub \
-h "$MQTT_HOST" \
-u "$MQTT_USER" \
-P "$MQTT_PASS" \
-I "$MQTT_CLIENT_ID_PREFIX" \
-t "$topic" \
-q 1 \
-m "$@" \
|| info "Error: Failed to publish, ignoring"
}
start_updating_state() {
(
previous=""
while true ; do
sleep "$POLL_MODE_PERIOD_S"
local state
if ! state="$(pcp mode 2>&1)" ; then
state="Failed: $?: $state"
fi
if [[ "$previous" != "$state" ]]; then
previous="$state"
publish "$MQTT_TOPIC_MODE" "$state"
fi
done
)&
PID="$!"
stop_updating_state() {
kill "$PID"
}
run_at_exit stop_updating_state
}
publish_LWT_offline() {
publish "$MQTT_TOPIC_WILL" "$MQTT_WILL_OFFLINE" --retain
publish "$MQTT_TOPIC_MODE" ""
}
subscribe_to_commands() {
info "<= Subscribing to '$MQTT_TOPIC_COMMAND'..."
mosquitto_sub \
-h "$MQTT_HOST" \
-u "$MQTT_USER" \
-P "$MQTT_PASS" \
-I "$MQTT_CLIENT_ID_PREFIX" \
-t "$MQTT_TOPIC_COMMAND" \
-q 2 \
--will-retain \
--will-payload "$MQTT_WILL_OFFLINE" \
--will-qos 1 \
--will-topic "$MQTT_TOPIC_WILL" \
| while read -r CMD ARG REST ; do
[[ -n "$CMD" ]] || continue
info "<= Got command: '$CMD' arg: '$ARG' rest: '$REST'"
if [[ -n "$REST" ]]; then
info "!! Error: too many arguments"
elif [[ -n "$ARG" ]] && [[ "$ARG" != "on" ]] && [[ "$ARG" != "off" ]] && ! [[ "$ARG" =~ ^[0-9]+$ ]]; then
info "!! Error: invalid argument '$ARG'"
elif [[ "$ALLOWED_PCP_COMMANDS" != *",$CMD,"* ]]; then
info "!! Error: command is not in ALLOWED_PCP_COMMANDS"
else
# ARG is now shell safe, either only a number, or "on" or "off", but don't pass unless needed
pcp "$CMD" $ARG || info "!! Error: command failed with $?"
fi
done
}
start_updating_state
run_at_exit publish_LWT_offline
publish "$MQTT_TOPIC_WILL" "$MQTT_WILL_INIT"
[[ -z "${VOLUME_AT_STARTUP:-}" ]] || pcp volume 0
[[ "${PAUSE_AT_STARTUP:-false}" == "false" ]] || pcp pause
[[ "${STOP_AT_STARTUP:-false}" == "false" ]] || pcp stop
[[ -z "${VOLUME_AT_STARTUP:-}" ]] || pcp volume "$VOLUME_AT_STARTUP"
while true ; do
publish "$MQTT_TOPIC_WILL" "$MQTT_WILL_ONLINE" --retain
subscribe_to_commands || (
info "Error: Subscribe failed, restarting in ${RETRY_SUBSCRIBE_AFTER_S}s..."
sleep "$RETRY_SUBSCRIBE_AFTER_S"
)
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment