XE3000 – Poweroff via CLI/Script only reboots

Hi everyone,

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?

Thanks in advance for any hints or workarounds!

Unfortunately it's not supported on the XE300(0) hardware:

@bruce This is true for the XE3000 as well, right?

Hi,

The MCU of XE300 is old so it does not support, but the new MCU of XE3000 does. :sweat_smile:

The XE3000 shutdown command is:

ubus call mcu cmd_json '{"power_off":"1"}'

If possible, please share your script. I think it may will help more XE3000 users.
Thank you in advance!

1 Like

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.” :grinning_face_with_smiling_eyes:
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:

  1. Threshold: As soon as the battery drops below a certain level, the router shuts down (but only if it’s not charging).

  2. 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 :smiley:

#!/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