I’m trying to completely power down my GL-XE3000 from the command line or within a script.
However, poweroff and halt only cause a reboot, not an actual shutdown.
Use case:
The router is installed in a vehicle, and I want a script to safely shut it down when no supply voltage is detected. The script itself is already working fine — but since poweroff just reboots, it’s not achieving the intended result.
Has anyone found a way to properly power off the XE3000 via CLI or another method?
The XE3000 turned out to be the wrong router for my use case. I thought, “better to have a battery and not need it than need one and not have it.”
Its purpose is to be used in my work vehicle. Since the on-board power socket gets voltage every time a door is opened, I had to write the following script. It basically consists of two parts:
Threshold: As soon as the battery drops below a certain level, the router shuts down (but only if it’s not charging).
Charge: After driving long enough, this mode becomes active; once charging stops, the router shuts down.
Please note that my coding skills are at “script kiddie” level — so don’t judge too harshly. The logging function was written by AI; the rest I did myself.
The MCU seems to have an update cycle of about 30 seconds, so I don’t think a check interval shorter than 10 seconds makes sense. I also hard-coded two 60-second delays to give me two minutes of buffer time in case of a boot loop due to misconfiguration.I switched to using ticks because I couldn’t get POSIX time calculations to work reliably.
P.S. Regarding the Discord log: it’s a bit unreliable, and sometimes the second message might arrive before the first one. But it’s still nice to have—it lets me know whenever someone’s been in my car
#!/bin/sh
# PowerGuard - Battery monitoring and shutdown script for GL-iNet XE3000
# POSIX shell compatible
# ---- Logging (RFC 5424-style levels) ---------------------------------------
LOG_TAG="powerguard" # syslog tag
LOG_TO_STDERR=1 # also print to stderr (0/1)
LOG_LEVEL=6 # default INFO (0..7). Set to 7 for DEBUG.
LOG_TO_SYSLOG=1 # also send to 'logger' (0/1)
# Default values/Options
CHECK_INTERVAL=10
BATTERY_THRESHOLD_ENABLED=1 # Enable battery threshold checking
BATTERY_THRESHOLD=95 # Battery percentage threshold for shutdown debug 95 empfehlung 80
BATTERY_THRESHOLD_SHUTDOWN_DELAY=60 # Delay before shutdown when threshold is reached (in seconds)
CHARGE_ENABLED=1 # Enable charge status checking
CHARGE_ARM_TIME=20 # Time to arm charge shutdown (in seconds)
CHARGE_SHUTDOWN_DELAY=60 # Delay before shutdown when charge is lost (in seconds)
DISCORD_WEBHOOK="" # Optional: Discord webhook URL for notifications
# State variables
BATTERY_THRESHOLD_SHUTDOWN_DELAY_MAX_TICKS=0 # computed from delay / check interval
BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS=0 # current tick count (incremented when triggered)
CHARGE_ARMING_MAX_TICKS=0
CHARGE_ARMING_TICKS=0
CHARGE_SHUTDOWN_DELAY_TICKS=0
CHARGE_SHUTDOWN_DELAY_MAX_TICKS=0
CHARGE_ARMED=0
MCU_BATTERY_CHARGE_PERCENT=0 # Battery charge percentage reported by MCU
MCU_BATTERY_CHARGING_STATUS=0 # Charging status reported by MCU
MCU_FIELD_BATTERY_CHARGING_STATUS="charging_status" # Field name for charging status in MCU JSON
MCU_FIELD_BATTERY_CHARGE_PERCENT="charge_percent" # Field name for battery charge percentage in MCU JSON
# Helpers
# Optional: minimal color for TTY stderr (never for syslog)
if [ -t 2 ]; then
C_DBG='\033[2m'; C_INFO='\033[36m'; C_WARN='\033[33m'; C_ERR='\033[31m'; C_RST='\033[0m'
else
C_DBG=''; C_INFO=''; C_WARN=''; C_ERR=''; C_RST=''
fi
is_uint() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
_log_level_to_num() {
# Input: level name or number; Output: echo number 0..7 (or 8 for unknown)
case "$1" in
0|EMERG|emerg) echo 0 ;;
1|ALERT|alert) echo 1 ;;
2|CRIT|crit) echo 2 ;;
3|ERR|ERROR|err|error) echo 3 ;;
4|WARN|WARNING|warn|warning) echo 4 ;;
5|NOTICE|notice) echo 5 ;;
6|INFO|info) echo 6 ;;
7|DEBUG|debug) echo 7 ;;
*) echo 8 ;;
esac
}
_log_num_to_name() {
case "$1" in
0) echo EMERG ;;
1) echo ALERT ;;
2) echo CRIT ;;
3) echo ERR ;;
4) echo WARNING ;;
5) echo NOTICE ;;
6) echo INFO ;;
7) echo DEBUG ;;
*) echo UNKNOWN ;;
esac
}
log() {
# Usage: log LEVEL "message ..."
# LEVEL: 0..7 or name (INFO/DEBUG/...)
lvl="$(_log_level_to_num "$1")"; shift
[ -z "$*" ] && return 0
# Drop if below threshold
[ "$lvl" -le "$LOG_LEVEL" ] || return 0
# Timestamp (RFC3339-ish without timezone colon for BusyBox)
ts="$(date '+%Y-%m-%dT%H:%M:%S%z')"
name="$(_log_num_to_name "$lvl")"
# Choose color for stderr
case "$lvl" in
0|1|2|3) color="$C_ERR" ;;
4) color="$C_WARN" ;;
5|6) color="$C_INFO" ;;
7) color="$C_DBG" ;;
*) color="" ;;
esac
if [ "${LOG_TO_STDERR:-1}" -eq 1 ]; then
# stderr: human-friendly
printf '%b%s [%s] %s%b\n' "$color" "$ts" "$lvl" "$*" "$C_RST" >&2
fi
if [ "${LOG_TO_SYSLOG:-1}" -eq 1 ] && command -v logger >/dev/null 2>&1; then
# syslog: machine-friendly with priority
# Map numeric to logger's -p facility.level (use user.* facility)
case "$lvl" in
0) pri="user.emerg" ;;
1) pri="user.alert" ;;
2) pri="user.crit" ;;
3) pri="user.err" ;;
4) pri="user.warning" ;;
5) pri="user.notice" ;;
6) pri="user.info" ;;
7) pri="user.debug" ;;
*) pri="user.notice" ;;
esac
logger -t "$LOG_TAG" -p "$pri" -- "$*"
fi
# --- optional Discord webhook (fire-and-forget) ---
if [ -n "$DISCORD_WEBHOOK" ]; then
(
# Fully detached, silent, non-blocking
exec >/dev/null 2>&1
# Prepare JSON payload; escape double quotes and backslashes
msg_escaped=$(printf '%s' "[$ts]" " " "[$lvl]" " " "$*" | sed 's/\\/\\\\/g; s/"/\\"/g')
printf '{"content":"[%s] %s - %s"}' "$name" "$msg_escaped" |
curl -s -m 20 --connect-timeout 1 --retry 0 \
-H 'Content-Type: application/json' \
--data-binary @- "$DISCORD_WEBHOOK" >/dev/null 2>&1
) &
fi
}
log_emerg() { log EMERG "$*"; }
log_alert() { log ALERT "$*"; }
log_crit() { log CRIT "$*"; }
log_err() { log ERR "$*"; }
log_warn() { log WARNING"$*"; }
log_notice() { log NOTICE "$*"; }
log_info() { log INFO "$*"; }
log_debug() { log DEBUG "$*"; }
router_shutdown(){
log_info "Initiating router shutdown..."
sleep 15
ubus call mcu cmd_json '{"power_off":"1"}'
}
# Get MCU status
get_mcu_status() {
# Fetch JSON from MCU
json=$(ubus call mcu status)
# Parse JSON fields
log_debug "MCU status JSON: $json"
MCU_BATTERY_CHARGE_PERCENT=$(echo "$json" | jsonfilter -e "@.${MCU_FIELD_BATTERY_CHARGE_PERCENT}")
MCU_BATTERY_CHARGING_STATUS=$(echo "$json" | jsonfilter -e "@.${MCU_FIELD_BATTERY_CHARGING_STATUS}")
#validate values
if ! is_uint "$MCU_BATTERY_CHARGE_PERCENT"; then
log_err "Invalid MCU battery charge percent: $MCU_BATTERY_CHARGE_PERCENT"
return 1
fi
if [ "$MCU_BATTERY_CHARGE_PERCENT" -lt 0 ] || [ "$MCU_BATTERY_CHARGE_PERCENT" -gt 100 ]; then
log_err "Invalid MCU battery charge percent: $MCU_BATTERY_CHARGE_PERCENT"
return 1
fi
if ! is_uint "$MCU_BATTERY_CHARGING_STATUS"; then
log_err "Invalid MCU battery charging status: $MCU_BATTERY_CHARGING_STATUS"
return 1
fi
return 0
}
# Check battery treshold
check_threshold() {
# Reset counter if above threshold or charging
if [ "$MCU_BATTERY_CHARGE_PERCENT" -gt "$BATTERY_THRESHOLD" ]; then
BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS=0
log_debug "THR:Battery above threshold (${MCU_BATTERY_CHARGE_PERCENT}% > ${BATTERY_THRESHOLD}%), no shutdown."
return 0
fi
# Reset counter if charging
if [ "$MCU_BATTERY_CHARGING_STATUS" -ne 0 ]; then
BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS=0
log_debug "THR:Battery is charging, no shutdown."
return 0
fi
# Increment counter
log_debug "THR:Battery below threshold (${MCU_BATTERY_CHARGE_PERCENT}% <= ${BATTERY_THRESHOLD}%) and not charging, incrementing shutdown delay tick (${BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS}/${BATTERY_THRESHOLD_SHUTDOWN_DELAY_MAX_TICKS})"
BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS=$((BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS + 1))
if [ "$BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS" -ge "$BATTERY_THRESHOLD_SHUTDOWN_DELAY_MAX_TICKS" ]; then
log_notice "THR:Battery threshold shutdown delay exceeded, shutting down router."
router_shutdown
fi
}
# Check charging status
check_charge() {
# Arm charge shutdown if charging sustained
if [ "$CHARGE_ARMED" -eq 0 ]; then
log_debug "CHA:Charge shutdown not yet armed."
# Check if charging
if [ "$MCU_BATTERY_CHARGING_STATUS" -eq 1 ]; then
CHARGE_ARMING_TICKS=$((CHARGE_ARMING_TICKS + 1))
log_debug "CHA:Battery is charging, incrementing charge arming tick (${CHARGE_ARMING_TICKS}/${CHARGE_ARMING_MAX_TICKS})"
else # Reset counter if not charging
CHARGE_ARMING_TICKS=0
log_debug "CHA:Battery is not charging, resetting charge arming tick."
fi
# Check if charged long enough to arm
if [ "$CHARGE_ARMING_TICKS" -gt "$CHARGE_ARMING_MAX_TICKS" ]; then
CHARGE_ARMED=1
log_info "CHA:Charge shutdown armed after ${CHARGE_ARMING_TICKS} ticks."
fi
fi
# If armed, check for charging stopped count tick high and when tick high enough shutdown
if [ "$CHARGE_ARMED" -eq 1 ]; then
# Check if not charging
log_debug "CHA:Charge shutdown is armed."
if [ "$MCU_BATTERY_CHARGING_STATUS" -eq 0 ]; then
CHARGE_SHUTDOWN_DELAY_TICKS=$((CHARGE_SHUTDOWN_DELAY_TICKS + 1))
log_debug "CHA:Battery is not charging, incrementing charge shutdown delay tick (${CHARGE_SHUTDOWN_DELAY_TICKS}/${CHARGE_SHUTDOWN_DELAY_MAX_TICKS})"
else # Reset counter if charging resumed
CHARGE_SHUTDOWN_DELAY_TICKS=0
log_debug "CHA:Battery is charging , resetting charge shutdown delay tick."
fi
# Check if shutdown delay exceeded
if [ "$CHARGE_SHUTDOWN_DELAY_TICKS" -gt "$CHARGE_SHUTDOWN_DELAY_MAX_TICKS" ]; then
log_notice "CHA:Charge shutdown delay exceeded, shutting down router."
router_shutdown
fi
fi
}
# Precompute max ticks
BATTERY_THRESHOLD_SHUTDOWN_DELAY_MAX_TICKS=$(( (BATTERY_THRESHOLD_SHUTDOWN_DELAY + CHECK_INTERVAL - 1) / CHECK_INTERVAL ))
CHARGE_ARMING_MAX_TICKS=$(( (CHARGE_ARM_TIME + CHECK_INTERVAL - 1) / CHECK_INTERVAL ))
CHARGE_SHUTDOWN_DELAY_MAX_TICKS=$(( (CHARGE_SHUTDOWN_DELAY + CHECK_INTERVAL - 1) / CHECK_INTERVAL ))
log_info "Initial 60s pause to allow MCU boot/update cycle (~30s) to complete."
sleep 60
log_notice "PowerGuard started with check interval ${CHECK_INTERVAL}s, battery threshold enabled ${BATTERY_THRESHOLD_ENABLED}, battery threshold ${BATTERY_THRESHOLD}%, battery threshold shutdown delay ${BATTERY_THRESHOLD_SHUTDOWN_DELAY}s, charge enabled ${CHARGE_ENABLED}, charge arm time ${CHARGE_ARM_TIME}s, charge shutdown delay ${CHARGE_SHUTDOWN_DELAY}s"
# Main loop
while true; do
if get_mcu_status; then
log_debug "MCU Battery: ${MCU_BATTERY_CHARGE_PERCENT}% , Charging Status: ${MCU_BATTERY_CHARGING_STATUS}"
else
log_err "Failed to get MCU status"
exit 1
fi
log_info "MCU Battery: ${MCU_BATTERY_CHARGE_PERCENT}% , Charging Status: ${MCU_BATTERY_CHARGING_STATUS}, Charge Armed: ${CHARGE_ARMED}, Charge Shutdown Delay Ticks: ${CHARGE_SHUTDOWN_DELAY_TICKS}/${CHARGE_SHUTDOWN_DELAY_MAX_TICKS},threshold Shutdown Delay Ticks: ${BATTERY_THRESHOLD_SHUTDOWN_DELAY_TICKS}/${BATTERY_THRESHOLD_SHUTDOWN_DELAY_MAX_TICKS}"
if [ "$BATTERY_THRESHOLD_ENABLED" = "1" ]; then
check_threshold
fi
if [ "$CHARGE_ENABLED" = "1" ]; then
check_charge
fi
sleep "$CHECK_INTERVAL"
done