Automatically putting a QNAP NAS to sleep when idle

I have a QNAP NAS (TS-453A) that only gets used occasionally. When it’s not being accessed, I want it to automatically go to sleep so that it doesn’t consume power unnecessarily. I’m OK with having to wait for the system to resume and the disks to spin up again before accessing files.

The only method of controlling when a QNAP QTS powered NAS goes to sleep is either by manually putting the device to sleep or scheduling a sleep time via the web interface.

To solve this issue for me, I have written a shell script which checks for certain system conditions, and if it detects that the system is idle then it will suspend. The conditions it checks for are:

  • > 30 minutes since resumed/booted
  • < 1.7 load average over 5, 10 and 15 minutes
  • No clients connected over SMB
  • No clients connected over NFS
  • < 40 packets per second send/received over 3 minutes

Installation Instructions

To install on a QNAP QTS powered NAS (tested on TS-453a running QTS 5.0.1):

  1. Copy the script to an area on a shared drive. It doesn’t have to be public, but it needs to be in a location that won’t be wiped by a QTS upgrade.
  2. Install vnstat. This is required for determining network activity; if you don’t care about this you can comment out the line beginning check_packets_threshold towards the bottom of the script, and skip to step 3.
    • Install entware-std from the Qnapclub store. This is required for the vnstat program which is used to determine network activity. If you don’t know how to do that, read this guide.
    • From an SSH shell, install vnstat:
      opkg install vnstat
  3. Make the script executable. You will need to first know the script’s location:
    chmod +x /share/CACHEDEV2_DATA/Misc/suspendIfInactive.sh
  4. Perform a dry-run using the ‘-d’ option to make sure that everything is working. This will output the checks that have been made and whether the system would be suspended, but will not actually suspend yet:
    /share/CACHEDEV2_DATA/Misc/suspendIfInactive.sh -d
  5. If all goes well, add the script to the crontab by adding the following line to /etc/config/crontab:
    */5 * * * * /share/CACHEDEV2_DATA/Misc/suspendIfInactive.sh

    You might be tempted to edit it by running crontab -e, but don’t as it will not persist after a QTS upgrade!

Script

#!/bin/bash
SUSPENDTHRESHOLD=1800
LOADTHRESHOLD=1.7
PACKETSTHRESHOLD=40
NFS_PORT=2049
SMB_PORT=445
NETWORKINTERFACE=bond0
DRY_RUN=false
# Color escape sequences
RED='\033[0;31m'
NC='\033[0m' # No Color
check_load_threshold() {
local load_name=$1
local load_value=$2
local threshold=$3
# Use awk for decimal comparisons since load average values are in decimal format
if awk -v load="$load_value" -v threshold="$threshold" 'BEGIN { exit !(load < threshold) }'; then
return 0 # Load condition is below threshold, indicating success
else
echo "Load condition failed for ${load_name}: ${load_value} >= ${threshold}"
return 1 # Load condition exceeds threshold, indicating failure
fi
}
check_load_thresholds() {
echo "Checking load thresholds..."
local LOAD=$(cat /proc/loadavg)
local load_avg_1min=$(echo "$LOAD" | awk '{printf "%.2f", $1}')
local load_avg_5min=$(echo "$LOAD" | awk '{printf "%.2f", $2}')
local load_avg_15min=$(echo "$LOAD" | awk '{printf "%.2f", $3}')
echo "Load Average (1min/5min/15min): $load_avg_1min $load_avg_5min $load_avg_15min"
failed_conditions=()
if ! check_load_threshold "1-minute Load Average" "$load_avg_1min" "$LOADTHRESHOLD"; then
failed_conditions+=("1-minute Load Average")
fi
if ! check_load_threshold "5-minute Load Average" "$load_avg_5min" "$LOADTHRESHOLD"; then
failed_conditions+=("5-minute Load Average")
fi
if ! check_load_threshold "15-minute Load Average" "$load_avg_15min" "$LOADTHRESHOLD"; then
failed_conditions+=("15-minute Load Average")
fi
if [ ${#failed_conditions[@]} -eq 0 ]; then
return 0 # Success: All load conditions are below threshold
else
echo "Load condition(s) failed."
return 1 # Failure: One or more load conditions exceed threshold
fi
}
check_clients_connected() {
local port=$1
echo -n "Checking clients connected on port ${port}... "
local connected_clients=$(netstat -an | grep ":${port}" | grep "ESTABLISHED" | wc -l)
echo "$connected_clients"
if [ "$connected_clients" -ge 1 ]; then
echo "Multiple clients connected to port ${port}: $connected_clients"
return 1 # failure
else
return 0 # success
fi
}
check_packets_threshold() {
echo -n "Checking for average network utilisation... "
local PACKETS=$(/opt/bin/vnstat -tr 180 -s -i "$NETWORKINTERFACE" | awk '$1 ~ /(r|t)x/ {sum+= $4} END {print sum}')
echo "${PACKETS} packets"
if [ "$PACKETS" -lt "$PACKETSTHRESHOLD" ]; then
echo "Packet condition passed."
return 0 # success
else
echo "Packet condition failed."
return 1 # failure
fi
}
suspend() {
echo "Suspending"
echo "mem" > /sys/power/state
}
check_time_since_suspend() {
echo "Checking time since last suspension..."
local suspend_threshold=$1
NOW=$(date +%s)
LASTSUSPENDDATE=$(cat /mnt/HDA_ROOT/.logs/kmsg | grep "PM: suspend exit" | tail -n1 | awk '{print $1, $2, $3}')
LASTSUSPENDUNIX=$(date -d "$LASTSUSPENDDATE" +%s)
TIMESINCESUSPEND=$(dc $NOW $LASTSUSPENDUNIX - p)
if [ -z "$TIMESINCESUSPEND" ]; then
TIMESINCESUSPEND=0
fi
echo "Suspended $TIMESINCESUSPEND seconds ago"
if [ "$TIMESINCESUSPEND" -gt "$suspend_threshold" ]; then
return 0 # success
else
echo "Time since last suspend is less than the threshold."
return 1 # failure
fi
}
# Process command-line options
while getopts ":d" opt; do
case $opt in
d) # Option '-d' is specified, indicating dry-run mode
DRY_RUN=true
;;
\?) # Invalid option encountered
echo -e "${RED}Invalid option: -$OPTARG${NC}" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1)) # Shifts the positional parameters to exclude processed options
# Perform all the checks, but skip suspension if any check fails
all_checks_pass=true
check_time_since_suspend "$SUSPENDTHRESHOLD" || { echo -e "${RED}Time since last suspend is less than the threshold.${NC}"; all_checks_pass=false; }
check_load_thresholds || { echo -e "${RED}Load condition(s) failed.${NC}"; all_checks_pass=false; }
check_clients_connected "$SMB_PORT" || { echo -e "${RED}Multiple clients connected to port ${SMB_PORT}.${NC}"; all_checks_pass=false; }
check_clients_connected "$NFS_PORT" || { echo -e "${RED}Multiple clients connected to port ${NFS_PORT}.${NC}"; all_checks_pass=false; }
check_packets_threshold || { echo -e "${RED}Packet condition failed.${NC}"; all_checks_pass=false; }
# Check if all checks passed
if [ "$all_checks_pass" = true ]; then
if [ "$DRY_RUN" = true ]; then
echo "Performing dry-run (no actual suspension)"
else
suspend
fi
else
echo "${RED}One or more conditions failed. Skipping suspension.${NC}"
fi

Leave a Reply