New night / day sunrise / sunset sunwait Bash script. API 2.0 tokens only

Previous development branch now released as 1.36
Locked
tsp84
Posts: 227
Joined: Thu Dec 24, 2020 4:04 am

New night / day sunrise / sunset sunwait Bash script. API 2.0 tokens only

Post by tsp84 »

I was using the old sunwait script on this forum and decided to jazz it up a little bit. This script assumes a few things and will break otherwise.
  • Only usable with API 2.0 access/refresh tokens
  • Requires: jq, sunwait, curl, bc
  • ZM needs to be setup with a hash passphrase, not append ips to hash, and login with authenticated 2.0 users (like eventserver install calls for)
  • Obviously API enabled
  • Run state setup for daytime named Day
  • Run state setup for night named Night
  • I run it with sudo -u www-data
You can see where to edit the user, passwd, logging, etc.. logging is a bool true/false, if false it only echos out to console.
This gets an access token and saves when it expires so it can keep using the access token or refresh it using the refresh token. This way you're not logging in all the time. can call this script with arguments "auth" (ex: sudo -u www-data ./zmsunwait.sh auth) to just get/refresh/display your tokens. call it with change <Night/Day> to force a state change. Though if you have a cronjob or systemd timer running this script it will change it on you anyways.

I had a root cronjob using sudo to run this script with user www-data (sudo -u www-data) every 10 or 15 minutes. This way if you reboot or anything happens your run states are still followed. I now have a systemd .timer linked to a .service file that runs its every 586 seconds (why not?).

Script in a gist available -> https://gist.github.com/tylersprice84/1 ... 85c21e19e9

Also will put in a code block in a reply post.
Last edited by tsp84 on Tue Mar 23, 2021 5:28 am, edited 1 time in total.
tsp84
Posts: 227
Joined: Thu Dec 24, 2020 4:04 am

Re: New night / day sunrise / sunset sunwait Bash script. API 2.0 tokens only

Post by tsp84 »

Code: Select all

#!/bin/bash
# call this with a cronjob every 10 mins from root but assigned www-data user - *10 * * * * /usr/bin/sudo -u www-data /path/to/this/script OR
# make a systemd .timer and .service file and use systemctl enable --now yourfileyoumade.timer. --now executes script now and starts timer
# instead of at next boot. Make sure systemd .service file uses sudo -u www-data as well.
# call this script with no args to check if run state is correct for time of day (involves logging into API)
# Must be using authenticated login and api 2.0
# call this script with "change <Night/Day>" to force a run state change
# call with "auth" to check auth and echo info out to console
# REQUIRES: sunwait, bc, curl, and jq. most repos have everything but sunwait -> https://github.com/risacher/sunwait
[[ ! $(command -v jq) ]] && echo "*** DEPENDANCY ISSUE: You need to install: jq (JSON Parser/Filter) ***" && echo "Exiting..." && exit 1
[[ ! $(command -v sunwait) ]] && echo "*** DEPENDANCY ISSUE: You need to install: sunwait ***" && echo "Exiting..." && exit 1
[[ ! $(command -v bc) ]] && echo "*** DEPENDANCY ISSUE: You need to install: bc ***" && echo "Exiting..." && exit 1
[[ ! $(command -v curl) ]] && echo "*** DEPENDANCY ISSUE: You need to install: curl ***" && echo "Exiting..." && exit 1

###         "EDIT THESE"
user=api_user #user name for zoneminder login
pass=api_password #password for zoneminder login
api_url='https://urZMinstall.duckdns.org' #your base url NOTICE NO TRAILING SLASH!!!

logging=false #log to file and echo or echo only
logfile='/home/jimmyjoebob/.local/zm/zm_check_sunwait.log'
auth_file='/home/jimmyjoebob/.local/zm/zm_auth.json'

lat= #Latitude for sunwait
lon= #Longitude for sunwait
offset=00:05 #Offset towards noon (x time after sunrise/before sunset)

###
##" Your run state in zm must be Day for daytime and Night for night. only these 2 supported for now with this script"
-------------------------------------------------------------------------------------------------------------------



want_state=(Night Day) #which states we want to watch for stored in an array
zm_status=$(sudo -u www-data $(which zmpkg.pl) status 2>&1)
sunwait=$(which sunwait)
auto='Forced'
[[ ! -e $logfile ]] && install -m 664 /dev/null $logfile
function logout() { if [[ "$logging" == true ]]; then echo -e "$@" >> $logfile; fi;  echo -e "$@"; }

#api auth wrapper
function api_auth() {

  curl_url=$api_url'/zm/api/host/login.json' #api login
  curl=$(which curl)
  curl_args=(-XPOST -d user=$user\&pass=$pass\&stateful=1 -sSf $curl_url) #Initial AUTH request format

  function create_tknfile() {
    [[ -e $auth_file ]] && $(which rm) $auth_file
    install -m 640 /dev/null $auth_file
    [[ $? -ne 0 ]] && logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: While creating $auth_file - exiting..." && return 1
  }

  function convsec {
    secs=$2
    [[ $1 =~ (day|long) ]] && ret_val=$(printf '%dd:%dh:%dm:%ds\n' $((secs/86400)) $((secs%86400/3600)) $((secs%3600/60)) $((secs%60)))
    [[ $1 =~ (nano|sec) ]] && ret_val=$(printf '%02dh:%02dm:%02fs\n' $(echo -e "$secs/3600\n$secs%3600/60\n$secs%60"| bc))
    [[ $1 =~ (reg|hour|min) ]] && ret_val=$(printf '%dh:%dm:%ds\n' $((secs/3600)) $((secs%3600/60)) $((secs%60)))
    echo "$ret_val"
  }

  function get_tokens () {
    auth_token=($(cat $auth_file)) #read token file into array so we can do things with it
    [[ $? -ne 0 ]] && logout "$(date '+%c'):[$$]:DBG:${FUNCNAME[0]}: couldnt read auth_file, get_tokens() auth_token: $auth_token" && return 1
      auth_key=$(jq -r '.access_token' <<< ${auth_token[0]})
      refresh_key=$(jq -r '.refresh_token' <<< ${auth_token[0]})
      auth_howlong=$(jq -r '.access_token_expires' <<< ${auth_token[0]})
      refresh_howlong=$(jq -r '.refresh_token_expires' <<< ${auth_token[0]})
      api_ver=$(jq -r '.apiversion' <<< ${auth_token[0]})
      auth_exp=$(jq -r '.auth_expires' <<< ${auth_token[0]})
      refresh_exp=$(jq -r '.refresh_expires' <<< ${auth_token[0]})
      grace=0 #set a grace period in seconds, set to 0 for none
      auth_grace=$(expr $auth_exp - $grace)
      if [[ $? -ne 0 ]]; then
        logout "$(date '+%c'):[$$]:DBG:${FUNCNAME[0]}: couldnt do math for auth_grace: auth_exp=$auth_exp - grace=$grace = auth_grace=$auth_grace"
        logout "$(date '+%c'):[$$]:DBG:${FUNCNAME[0]}: access_token_expires = $auth_howlong - refresh_token_expires = $refresh_howlong - auth_expires = $auth_exp"
        return 1
      fi
    }

  function auth() {
    get_tokens
    current=$(date -d $(date '+%T') '+%s')
    if  [[ $current -ge $refresh_exp ]]; then #is refresh expired?
      logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: REFRESH ($(convsec reg $(expr $current - $refresh_exp)) past) is expired! Getting new tokens (this should happen every $(convsec reg $refresh_howlong)"
      get_auth
      return 0
    elif [[ $current -ge $auth_grace ]]; then #is auth expired? with grace period
      logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: AUTH ($(convsec reg $(expr $current - $auth_exp)) past) is expired! - REFRESH ($(convsec reg $(expr $refresh_exp - $current))) is valid!"
      get_refresh
      return 0
    else #keep using the VALID auth token
    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: AUTH ($(convsec reg $(expr $auth_exp - $current))) is valid! - REFRESH ($(convsec reg $(expr $refresh_exp - $current))) is valid!"
    return 0
    fi
  }


  function get_auth() {
    auth_token=($($curl ${curl_args[@]}))
    curl_good=$(jq -r '.success' <<< "${auth_token[0]}")
    curl_good2=$(jq -r '. | length' <<< ${auth_token[0]})
    if [[ $curl_good = 'false' ]] || [[ $curl_good2 = 0 ]]; then
      logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: AUTH request FAILED: ${auth_token[@]} - exiting..."
      return 1
    fi
    create_tknfile
    auth_key=$(jq -r '.access_token' <<< "${auth_token[0]}")
    refresh_key=$(jq -r '.refresh_token' <<< "${auth_token[0]}")
    auth_howlong=$(jq -r '.access_token_expires' <<< "${auth_token[0]}")
    refresh_howlong=$(jq -r '.refresh_token_expires' <<< "${auth_token[0]}")
    api_ver=$(jq -r '.apiversion' <<< "${auth_token[0]}")
    current=$(date -d $(date '+%T') '+%s')
    auth_exp=$(expr $current + $auth_howlong)
    refresh_exp=$(expr $current + $refresh_howlong)

    echo "{\"access_token\":\"$auth_key\",\"access_token_expires\":$auth_howlong,\"refresh_token\":\"$refresh_key\",\"refresh_token_expires\":$refresh_howlong,\"apiversion\":\"$api_ver\",\"auth_expires\":\"$auth_exp\",\"refresh_expires\":\"$refresh_exp\"}" > $auth_file

    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: New AUTH ($(convsec reg $auth_howlong) lifetime) and REFRESH ($(convsec reg $refresh_howlong) lifetime) tokens grabbed!"
  }

  function get_refresh() {
    curl_args=(-XPOST -d token=$refresh_key -sSf $curl_url)
    refresh_auth_tkn=$($curl ${curl_args[@]})
    [[ $? -ne 0 ]] && logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: curl ${curl_args[@]} - exiting..." && return 1
    curl_good=$(jq -r '.success' <<< "$refresh_auth_tkn")
    curl_good2=$(jq -r '. | length' <<< ${refresh_auth_tkn[0]})
    if [[ $curl_good = 'false' ]] || [[ $curl_good2 = 0 ]]; then
      logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: AUTH request FAILED: ${auth_token[@]} - exiting..."
      return 1
    fi
#Do new access_token calculations
    auth_key=$(jq -r '.access_token' <<< "$refresh_auth_tkn")
    auth_howlong=$(jq -r '.access_token_expires' <<< "$refresh_auth_tkn")
    current=$(date -d $(date '+%T') '+%s')
    new_auth_exp=$(expr $current + $auth_howlong)
    create_tknfile
    echo "{\"access_token\":\"$auth_key\",\"access_token_expires\":$auth_howlong,\"refresh_token\":\"$refresh_key\",\"refresh_token_expires\":$refresh_howlong,\"apiversion\":\"$api_ver\",\"auth_expires\":\"$new_auth_exp\",\"refresh_expires\":\"$refresh_exp\"}" > $auth_file
    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: AUTH ($(convsec hour $auth_howlong) lifetime) token refreshed!"
  }


if  [[ -e $auth_file ]]; then
  auth
  return 0
else
  logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: No AUTH file ($auth_file), creating now with new AUTH/REFRESH tokens"
  get_auth
  return 0
fi
  return 0
}


function zmsunwait() {

  run_state=$2
  if [[ $run_state =~ ^(r|R)ise ]] || [[ $run_state =~ ^(d|D)ay ]]; then
    run_state="Day"
  elif [[ $run_state =~ ^(s|S)et ]] || [[ $run_state =~ ^(n|N)ight ]]; then
    run_state="Night"
  else
    echo "Usage: zm_check_sunwait.sh change <Day/Night>"
    exit 1
  fi
  if [[ $zm_status == "running" ]]; then
    $(which zmpkg.pl) $run_state
    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: $auto set ZoneMinder run state to $run_state"
  else
    logout "$(date '+%c'):[$$]:WAR:${FUNCNAME[0]}: ZoneMinder is not set to 'running', not changing run state, exiting..."
  fi
  return 0
}

function arg_eval() {
  [[ $1 = 'auth' ]] && api_auth && echo -e "Auth token ($(convsec reg $(expr $auth_exp - $current))): $auth_key" && echo -e "Refresh token ($(convsec reg $(expr $refresh_exp - $current))): $refresh_key" && echo -e "Tokens saved to: $auth_file" && exit 0
  zmsunwait $@
  [[ $? -ne 0 ]] && exit 1 || exit 0
}

#"check if there are any arguments, if so evaluate"
[[ $# > 0 ]] && arg_eval $@

# "Main var section"
sunrise=$(date -d $($sunwait list rise offset $offset $lat $lon) '+%T') # get sunrise and sunset times with x min offset
sunset=$(date -d $($sunwait list set offset $offset $lat $lon) '+%T')
#this is how we do math, with "unix/epoch time"
epoch_current=$(date -d $(date '+%T') '+%s')
epoch_sunrise=$(date -d $sunrise '+%s')
epoch_sunset=$(date -d $sunset '+%s')
epoch_before_rise=$(date -d 00:59:30 '+%s')
epoch_after_rise=$(date -d 01:00:30 '+%s')
epoch_before_set=$(date -d 12:59:30 '+%s')
epoch_after_set=$(date -d 13:00:30 '+%s')
epoch_before_midnight=$(date -d 23:59:59 '+%s')
epoch_after_midnight=$(date -d 00:00:59 '+%s')
# "do API auth token stuff"
  api_auth
[[ $? -ne 0 ]] && logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: Function api_auth returned an error: \$? = $?" && exit 1
# "Create request to get the current run state (also lists all states available)"
curl=$(which curl)
curl_url=$api_url"/zm/api/states.json?token=$auth_key" #url to query states with auth
curl_args=(-XGET $curl_url -sSf)
states_JSON=$($curl ${curl_args[@]}) #execute curl GET and store return in variable
[[ $? -ne 0 ]] && logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: Curl: trying to query for states (AUTH problem most likely). states_JSON= $states_JSON"
#echo "states_JSON = $states_JSON" #debug
#  "Parse JSON Data (specific to states, only to impliment DAY/NIGHT); can expand later?"
state_avail=$(jq -r '.[] | length' <<< $states_JSON)
[[ $states_avail = 0 ]] && logout "$(date '+%c'):[$$]:DBG:${FUNCNAME[0]}: states_avail = 0, whats wrong here? states_JSON = $states_JSON" && exit 1
#echo "Total # of States Available: $state_avail" #debug
state_names=($(jq -r '.states[].State.Name' <<< $states_JSON))
#echo "state_names in ARRAY = ${state_names[@]}" #debug
state_isactive=($(jq -r '.states[].State.IsActive' <<< $states_JSON))
#echo "state_isactive in ARRAY = ${state_isactive[@]}" #debug

# "loop to find which indice is active, this correlates to a name"
iter=0
for i in ${state_isactive[@]}
 do
  if  [[ $i = '1' ]]; then
    curr_state="${state_names[$iter]}"
    break
  fi
  iter=$(expr $iter + 1)
 done
# "make sure we have the info needed to continue on"
[[ -z $curr_state ]] && logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}:  curr_state is null, exiting..." && exit 1
#echo "curr_state = $curr_state" #debug

function zmsunwaitcheck {
  current=$(date '+%T') #HH:MM:SS
  if [[ $epoch_current -ge $epoch_after_midnight ]] && [[ $epoch_current -le $epoch_sunrise ]]; then # is it between midnight sunrise? it should be Night
    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: It's currently ($current) between Midnight and sunrise ($sunrise). Run state ($curr_state) should be Night, checking now"
    if [[ $curr_state != ${want_state[0]} ]]; then #if runstate isnt set to Night (1st [0] in array), theres an issue
      auto='Automatically'
      logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: State ($curr_state) is not set to Night, changing now and exiting..."
      zmsunwait change Night
      return 0
    else
      return 0
    fi
  elif [[ $epoch_current -ge $epoch_sunrise ]] && [[ $epoch_current -le $epoch_sunset ]]; then # is it between sunrise and sunset? should be Day
    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: It's currently ($current) between sunrise ($sunrise) and sunset ($sunset). Run state ($curr_state) should be Day, checking now"
      if [[ $curr_state != ${want_state[1]} ]]; then #if runstate isnt set to Day (2nd [1] in array), theres an issue
      auto='Automatically'
      logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: State ($curr_state) is not set to Day, changing now and exiting..."
      zmsunwait change Day
      return 0
      else
        return 0
      fi
  elif [[ $epoch_current -ge $epoch_sunset ]] && [[ $epoch_current -le $epoch_before_midnight ]]; then #is it between sunset and midnight? should be Night
    logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: It's currently ($current) between sunset ($sunset) and Midnight. Run state ($curr_state) should be Night, checking now"
    if [[ $curr_state != ${want_state[0]} ]]; then # if current state isnt set to Night (1st [0] in array), theres an issue
      auto='Automatically'
      logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: State ($curr_state) is not set to Night, changing now and exiting..."
      zmsunwait change Night
      return 0
    elif [[ $curr_state = ${want_state[0]} ]]; then
      return 0
    else
      logout "$(date '+%c'):[$$]:INF:${FUNCNAME[0]}: State ($curr_state) is not Night or Day! Expand me to handle this"
      return 0
    fi
  else
    logout "$(date '+%c'):[$$]:ERR:${FUNCNAME[0]}: Time stuff is messed up, come check script! Exiting with ERROR!"
    exit 1
  fi
}

#--------- MAIN LOGIC -----------
zmsunwaitcheck
tsp84
Posts: 227
Joined: Thu Dec 24, 2020 4:04 am

Re: New night / day sunrise / sunset sunwait Bash script. API 2.0 tokens only

Post by tsp84 »

Here is the sunwait github repo https://github.com/risacher/sunwait
Locked