#!/bin/bash # This script sets your computer to wake at a given time (h)h:mm nearest in the # future or recurrently for some general cron-formatted times. The computer is # actually woken up 5 minutes earlier than specified, and a given script is set # to run at the exact time given via crontab. This script must be run as root. # Copyright (C) 2012 David Glass # Copyright is GPLv3 or later, see /usr/share/common-licenses/GPL-3 # For only print next wakeup time if [[ $1 == "-p" ]]; then only_print=true shift fi if [[ ! $only_print ]]; then if [[ `whoami` != "root" ]]; then echo "setalarm must be run as root" exit 1 fi # Clear the system wakeup alarm. echo 0 > /sys/class/rtc/rtc0/wakealarm; # For delete alarms if [[ $1 == "-d" ]]; then (crontab -l | sed /^.*setalarm.*$/d) | crontab - exit 0 fi fi # Check if correct number of arguments given if [[ $1 == "-c" && $# -lt 6 && ! $only_print || $1 == "-u" && ! $2 =~ [0-9]+ || $1 != "-c" && $1 != "-u" && ! $1 =~ [0-9]{1,2}\:[0-9]{2} ]]; then echo "Usage: setalarm [-p] [hh:mm] [-u utc] [-c m h dom mon dow] [-o offset] [command]" echo "Asterisks may need to be surrounded by quotes" exit 1 fi if [[ $1 != "-c" && $2 == "-o" || $1 == "-c" && $7 == "-o" || $1 == "-u" && $3 == "-o" ]]; then if [[ $1 != "-c" && ! $3 =~ [0-9]+ || $1 == "-c" && ! $8 =~ [0-9]+ ]]; then if [[ $1 == "-u" && ! $4 =~ [0-9]+ ]]; then echo "Invalid offset value" exit 1 fi fi if [[ $1 == "-c" ]]; then offset=$8 elif [[ $1 == "-u" ]]; then offset=$4 else offset=$3 fi offset_set=true else offset=5 #minutes (default) fi # Get wakeup script if [[ ! $only_print ]]; then script_start=$(if [[ $1 == "-u" ]]; then echo 3; elif [[ $1 != "-c" ]]; then echo 2; else echo 7; fi) if [[ $offset_set ]]; then script_start=$(( script_start + 2 )); fi wakeup_script=${*:$script_start} fi # Check if an element exists in an array function check_element { for i in ${*:2}; do if [[ $i == $1 ]]; then echo 1; return; fi done echo 0 return } # Read in cron input function read_cron_input { if [[ $4 == "Months" ]]; then input=$(echo "$1" | sed -e 's/Sat/6/ig' \ -e 's/Jan/1/ig' -e 's/Feb/2/ig' -e 's/Mar/3/ig' -e 's/Apr/4/ig' \ -e 's/May/5/ig' -e 's/Jun/6/ig' -e 's/Jul/7/ig' -e 's/Aug/8/ig' \ -e 's/Sep/9/ig' -e 's/Oct/10/ig' -e 's/Nov/11/ig' -e 's/Dec/12/ig') elif [[ $4 == "Days" ]]; then input=$(echo "$1" | sed -e 's/Sun/0/ig' -e 's/Mon/1/ig' \ -e 's/Tue/2/ig' -e 's/Wed/3/ig' -e 's/Thu/4/ig' -e 's/Fri/5/ig' ) else input=$1 fi # Check for bad input: exit only exits function, need to check for "" output if [[ $input =~ [^\-\,0-9\*] ]]; then exit 1; fi if [[ $input == "*" ]]; then seq $2 $3 elif [[ $input =~ .*-.* ]]; then seq ${input/-/ } else echo $input | sed 's/,/\n/g' | sort -g fi } # for utc times if [[ $1 == "-u" ]]; then wake_time=$2 if [[ $only_print ]]; then echo $(( $wake_time - $offset * 60 )) exit fi crontab_time=`date -d @$wake_time "+%M %H %d %m %w"` newline="$crontab_time $wakeup_script >/dev/null 2>&1 #entered by setalarm" if [[ $wakeup_script != "" ]]; then (crontab -l | sed /^.*setalarm.*$/d; echo $newline) | crontab - fi # for recurrent alarms elif [[ $1 == "-c" ]]; then # Grab current date now_dom=$(date +%-d); now_mon=$(date +%-m); now_dow=$(date +%w) now_hr=$(date +%-H); now_min=$(date +%-M) # Read cron input mins=($(read_cron_input "$2" 0 59)) hrs=($(read_cron_input "$3" 0 23)) doms=($(read_cron_input "$4" $now_dom 31)) mons=($(read_cron_input "$5" $now_mon 12 "Months")) dows=($(read_cron_input "$6" 0 6 "Days")) # check if all values are valid if [[ $mins == "" || $hrs == "" || $doms == "" || $mons == "" || $dows == "" ]]; then echo "bad cron time entries" >&2; exit 1; fi last_hr=${hrs[$(( ${#hrs[@]} - 1 ))]} last_min=${mins[$(( ${#mins[@]} - 1 ))]} # Get the alarm month/day function chkw # check if date occurs on an acceptable day of week { check_element $(date --date @$wake_time +%w) ${dows[@]} } years=($(date +%Y) $(( $(date +%Y) + 1 ))) for y in ${years[@]}; do for i in ${mons[@]}; do for j in ${doms[@]}; do if [[ $(date -d $i/$j/$y +%s) -ge $(date -d $now_mon/$now_dom/${years[1]} +%s) ]]; then break; fi wake_time=$(date --date "$i/$j/$y $last_hr:$last_min:00" +%s); if [[ `chkw` != 1 ]]; then continue; fi date="$i/$j/$y" if [[ $wake_time > `date +%s` ]]; then break 3; fi done if [[ $4 == "*" ]]; then doms=$(seq 1 31); fi done if [[ $5 == "*" ]]; then mons=$(seq 1 $now_mon); fi done 2>/dev/null if [[ $date == "" || $wake_time < `date +%s` || `chkw` != 1 ]]; then echo "The specified cron time does not occur within a year" exit 1 fi # Get the hour:minute if [[ $date == $(date +%-m/%-d/%Y) ]]; then mins=($(read_cron_input "$2" $now_min 59)) hrs=($(read_cron_input "$3" $now_hr 23)) fi for k in ${hrs[@]}; do for l in ${mins[@]}; do wake_time=$(date --date "$date $k:$l:00" +%s); if [[ $wake_time > `date +%s` ]]; then break 2; fi done if [[ $2 == "*" ]]; then mins=$(seq 0 59); fi done 2>/dev/null if [[ $only_print ]]; then echo $(( $wake_time - $offset * 60 )) exit fi # add cron for setlarm that runs wtih the $wakeup_script and @reboot newline1="$2 $3 $4 $5 $6 $0 ${*/\*/\"*\"} >/dev/null 2>&1\n" newline2="@reboot $0 ${*/\*/\"*\"} >/dev/null 2>&1" if [[ $wakeup_script != "" ]]; then newline3="\n$2 $3 $4 $5 $6 $wakeup_script >/dev/null 2>&1 #entered by setalarm" fi (crontab -l | sed /^.*setalarm.*$/d; echo -e "$newline1$newline2$newline3") | crontab - # for non-recurring alarms else # determine a wakeup time. add a day if time has already passed for today wake_time=$(date --date "$(date +%D) $1:00" +%s) if [[ $wake_time < `date +%s` ]]; then wake_time=$(( $wake_time + 86400 )); fi if [[ $only_print ]]; then echo $(( $wake_time - $offset * 60 )) exit fi # format $wakeup_time in crontab format. crontab_time=`date -d @$wake_time "+%M %H %d %m %w"` if [[ $wakeup_script != "" ]]; then newline="$crontab_time $wakeup_script >/dev/null 2>&1 #entered by setalarm" (crontab -l | sed /^.*setalarm.*$/d; echo $newline) | crontab - fi fi # Actually set alarm # # Before we can actually set the alarm we need to account for potential # differences between local time and UTC because the rtc wakealarm uses # UTC regardless of how RTC is set. utc_offset=$(expr `date -u --date "01/01/01 12:00:00" +%s` - `date --date "01/01/01 12:00:00" +%s`) # Now we can actually set the alarm echo $(( $wake_time + $utc_offset - $offset * 60 )) > /sys/class/rtc/rtc0/wakealarm; echo -e "Alarm set to: $(date -d @$wake_time)" # Remind user about offset if [[ $offset_set ]]; then echo "Computer will wake $offset minutes earlier"; fi