commit d27a1be385f756bd2c52d43dc183407701cff183 Author: Rob Pearce Date: Thu Jun 3 09:01:36 2021 +1000 Initial checkin diff --git a/aircon.sh b/aircon.sh new file mode 100755 index 0000000..beccffa --- /dev/null +++ b/aircon.sh @@ -0,0 +1,1560 @@ +#!/bin/bash + +# TODO: comparison to previous reading +# TODO: gnuplot of given time range +# TODO: ditch pymyair +# TODO: manual controls + + +DEFAULT_KILLFILE="${HOME}/.aircon_noaction" +DEFAULT_CONFIGFILE=${HOME}/.airconrc +DEFAULT_CSVFILE=${HOME}/acstats.csv +DEFAULT_AIRCON_IP=10.99.99.1 +DEFAULTLIMIT=3 +DEFAULT_TOLERANCE=0 +VALID_MODES=" cool heat " +VALID_ZONE_STATES=" open close " + +BOLD="\033[1m" +PLAIN="\033[0m" +UNDERLINE="\033[4m" +GREY="\033[1;30m" +RED="\033[31m" +GREEN="\033[32m" +YELLOW="\033[33m" +BLUE="\033[34m" +CYAN="\033[36m" +WHITE="\033[37m" +LINK="$BLUE$UNDERLINE" + + +function enable_cronmode() { + cronmode=1 + BOLD="" + PLAIN="" + UNDERLINE="" + GREY="" + RED="" + GREEN="" + YELLOW="" + BLUE="" + CYAN="" + WHITE="" + LINK="$BLUE$UNDERLINE" +} + +function usage() { + echo "usage: $0 [options]" + echo + echo " Modifies aircon based on configured parameters in $DEFAULT_CONFIGFILE." + echo + echo " -h Show this text." + echo " -c Cron mode. Only show output if actions were taken." + echo " -i x.x.x.x Specify IP address for aircon (default is $DEFAULT_AIRCON_IP)" + echo " -I db Log all zone temperatures to given influxdb database, then exit (see -o)." + echo " -A db Log actions to given influxdb database." + echo " -f file Specify an alternate config file." + echo " -k file If file exists, never change aircon settings (default: $DEFAULT_KILLFILE)." + echo " -l num Set number of too hot/cold zones at which to taking action." + echo " -L Log all zone temperatures to CSV file, then exit (see -o)." + echo " -o file Specify CSV output file. Default: $DEFAULT_CSVFILE" + echo " -m Generate a config file based on current aircon setup." + echo " -p Profiler mode." + echo " -w List which zone owners' devices are available then exit." + echo " -t num Specify degrees below min temperature before taking action." + echo " -T num Specify degrees above max temperature before taking action." + echo " -y Actually run commands/db inserts. By default, commands are just displayed." + echo +} + +function action() { + echo -e "$BOLD$GREEN>> Running action: $PLAIN$GREEN$*$PLAIN" +} + +function error() { + echo -e "$BOLD${RED}ERROR: $PLAIN$RED$*$PLAIN" >/dev/stderr +} + +function info() { + echo -e "$BOLD${CYAN}>> $PLAIN$CYAN$*$PLAIN" +} + +# get_midrange zone_idx +function get_midrange() { # outputs midpoint of temperature range for given zone + local min max + min=${zwantmin[$idx]} + max=${zwantmax[$idx]} + echo "($min + $max) / 2" | bc +} + +function getoppositestate() { + if [[ $1 == "open" ]]; then + echo "close" + elif [[ $1 == "close" ]]; then + echo "open" + fi +} + +function getzoneaction() { # populates zproblem[] and zaction[] + local idx problem num thisaction priority pingok fv + local donearby + idx=$1 + + profile "getzoneaction for idx ${zname[$idx]}" + thisaction="n/a" + if [[ ${zignore[$idx]} -eq 1 ]]; then + problem="n/a" + else + if [[ -z ${zowner[$idx]} ]]; then + pingok=1 + elif canping "${zowner[$idx]}"; then + pingok=1 + else + pingok=0 + fi + fv=$(getforcevent ${zname[$idx]}) + + donearby=0 + + if [[ ! -z $fv && ${zstate[$idx]} == $(getoppositestate ${fv}) ]]; then + problem="force_${fv}" + priority=99 + elif [[ $airconmode != "off" && ${zstate[$idx]} != "close" && $pingok -eq 0 ]]; then + problem="owner_not_home" + priority=99 + elif [[ $pingok -eq 1 && $(echo "${ztemp[$idx]} > (${zwantmax[$idx]} + $tolerance_h)" | bc) -eq 1 ]]; then + problem="too_hot" + priority=$(echo "${ztemp[$idx]} - ${zwantmax[$idx]}" | bc) + if [[ $(echo "${ztemp[$idx]} > (${zwantmax[$idx]} + ($tolerance_h * 2))" | bc) -eq 1 ]]; then + donearby=1 + fi + elif [[ $pingok -eq 1 && $(echo "${ztemp[$idx]} < (${zwantmin[$idx]} - $tolerance_l)" | bc) -eq 1 ]]; then + problem="too_cold" + priority=$(echo "${zwantmin[$idx]} - ${ztemp[$idx]}" | bc) + if [[ $(echo "${ztemp[$idx]} < (${zwantmin[$idx]} - ($tolerance_l * 2))" | bc) -eq 1 ]]; then + donearby=1 + fi + else + problem="n/a" + priority=0 + fi + #info "${zname[$idx]} priority is $priority" + zproblem[$idx]="$problem" + zpri[$idx]="$priority" + + if [[ $problem == "owner_not_home" ]]; then + # turn the zone off + if [[ ${zstate[$idx]} != "close" ]]; then + thisaction="$thisaction close_vent" + fi + elif [[ $problem =~ force_ ]]; then + if [[ ${zstate[$idx]} != $fv ]]; then + thisaction="$thisaction ${fv}_vent" + fi + elif [[ $problem == "too_cold" ]]; then + if [[ $airconmode == "heat" || $airconmode == "off" ]]; then + # turn the system on, if required + if [[ $airconmode == "off" ]]; then + if [[ $modelock == "n/a" || $modelock == "heat" ]]; then + thisaction="$thisaction power_on set_mode:heat" + fi + fi + + # if we couldnt turn the system on, there's nothing else to do + if [[ $airconmode == "heat" ]]; then + # adjust set temperature, if required + if [[ $(echo "${zsettemp[$idx]} > ${zwantmax[$idx]}" | bc) -eq 1 || $(echo "${zsettemp[$idx]} < ${zwantmin[$idx]}" | bc) -eq 1 ]]; then + # set temperature to midpoint of acceptable range + num=$(get_midrange $idx) + if [[ $zsettemp != $num ]]; then + thisaction="$thisaction set_temp:$num" + fi + fi + # open the vent, if required + if [[ ${zstate[$idx]} != "open" ]]; then + thisaction="$thisaction open_vent" + fi + # set myzone, even if not required (will fix later) + thisaction="$thisaction set_myzone" + fi + elif [[ $airconmode == "cool" ]]; then + # set temperature to midpoint of acceptable range + num=$(get_midrange $idx) + if [[ $zsettemp != $num ]]; then + thisaction="$thisaction set_temp:$num" + fi + # close the vent, if required + if [[ ${zstate[$idx]} != "close" ]]; then + thisaction="$thisaction close_vent" + elif [[ $donearby -eq 1 ]]; then + thisaction="close_nearby_zone" + fi + fi + elif [[ $problem == "too_hot" ]]; then + if [[ $airconmode == "cool" || $airconmode == "off" ]]; then + # turn the system on, if required + if [[ $airconmode == "off" ]]; then + if [[ $modelock == "n/a" || $modelock == "cool" ]]; then + thisaction="$thisaction power_on set_mode:cool" + fi + fi + + # if we couldnt turn the system on, there's nothing else to do + if [[ $airconmode == "cool" ]]; then + # adjust set temperature, if required + if [[ $(echo "${zsettemp[$idx]} > ${zwantmax[$idx]}" | bc) -eq 1 || $(echo "${zsettemp[$idx]} < ${zwantmin[$idx]}" | bc) -eq 1 ]]; then + # set temperature to midpoint of acceptable range + num=$(get_midrange $idx) + if [[ $zsettemp != $num ]]; then + thisaction="$thisaction set_temp:$num" + fi + fi + # open the vent, if required + if [[ ${zstate[$idx]} != "open" ]]; then + thisaction="$thisaction open_vent" + fi + # set myzone, even if not required (will fix later) + thisaction="$thisaction set_myzone" + fi + elif [[ $airconmode == "heat" ]]; then + # set temperature to midpoint of acceptable range + num=$(get_midrange $idx) + if [[ $zsettemp != $num ]]; then + thisaction="$thisaction set_temp:$num" + fi + # close the vent, if required + if [[ ${zstate[$idx]} != "close" ]]; then + thisaction="$thisaction close_vent" + elif [[ $donearby -eq 1 ]]; then + thisaction="close_nearby_zone" + fi + fi + fi + fi + zaction[$idx]="$thisaction" + profile "getzoneaction for idx ${zname[$idx]}" +} + +function getidxfromname() { # name + local x + for x in ${!zname[@]}; do + if [[ ${zname[$x]} == $1 ]]; then + echo "${x}" + fi + done +} + +function getnamefromid() { # id + local x + for x in ${!zname[@]}; do + if [[ ${zid[$x]} == $1 ]]; then + echo "${zname[$x]}" + fi + done +} + +# getforcevent zone_name +function getforcevent() { + local x retval + retval="" + for x in ${!fv_zonename[@]}; do + if [[ ${fv_zonename[$x]} == "$1" ]]; then + retval="${fv_state[$x]}" + break + fi + done + echo "$retval" +} + +# getalladj zone_name +function getalladj() { + local x retval + retval="" + for x in ${!adj_zonename[@]}; do + if [[ ${adj_zonename[$x]} == "$1" ]]; then + retval="${adj_nearby[$x]}" + break + fi + done + echo "$retval" +} + +function find_nearby_zones() { # zone_idx filter1 filter2 ... filter is: open|closed|noaction + local idx thiszone nearbyzones allnearbyzones thisone zi ok db conditions + idx=$1 + shift + conditions="$*" + db=0 + if [[ $db -eq 1 ]]; then + echo "find_nearby_zones(): idx = $idx" >/dev/stderr + echo "find_nearby_zones(): conditions = [$conditions]" >/dev/stderr + fi + thiszone=${zname[$idx]} + allnearbyzones=$(getalladj ${thiszone}) + nearbyzones="" + if [[ $db -eq 1 ]]; then + echo "find_nearby_zones(): allnearbyzones = [$allnearbyzones]" >/dev/stderr + fi + for thisone in $allnearbyzones; do + ok=1 + zi=$(getidxfromname $thisone) + if [[ $conditions == *open* && ${zstate[$zi]} != "open" ]]; then + ok=0 + fi + if [[ $conditions == *closed* && ${zstate[$zi]} != "close" ]]; then + ok=0 + fi + if [[ $conditions == *noaction* && ${zaction[$zi]} != "n/a" ]]; then + ok=0 + fi + if [[ $ok -eq 1 ]]; then + nearbyzones="$nearbyzones $thisone" + fi + done + echo "$nearbyzones" +} + + +function generate_actions() { # populates global: nairconcommands & airconcmd[] + local x y z adj tempstr nhot ncold + local powerchange nmyzones maxpri bestidx + local globaction mzmustmove poss nomyzone + local db + db=0 + + nairconcommands=0 + powerchange="" + + for x in ${!zname[@]}; do + getzoneaction $x + done + + # pass 1 + profile "pass 1 start" + for x in ${!zname[@]}; do + thisaction="${zaction[$x]}" + IFS=' ' read -ra tok <<< "${zaction[$x]}" + for y in ${!tok[@]}; do + if [[ ${tok[$y]} == "close_nearby_zone" ]]; then + # is there a nearby zone that's open? + adj=$(find_nearby_zones $x open noaction) + if [[ -z $adj ]]; then + zaction[$x]="power_off" + if [[ -z $powerchange ]]; then + powerchange="power_off" + elif [[ ${zaction[$x]} != $powerchange ]]; then + zactionfail[$x]=1 + fi + else + tempstr="" + for z in $adj; do + tempstr="$tempstr close:$z" + done + zaction[$x]="${zaction[$x]/close_nearby_zone/$tempstr}" + fi + fi + done + done + + # pass 2 - If multiple zones are too hot/cold, turn the whole system off/on (if off, replace all actions) + nhot=0 + ncold=0 + for x in ${!zname[@]}; do + if [[ -z $powerchange ]]; then + if [[ ${zproblem[$x]} == "too_hot" ]]; then + nhot=$((nhot + 1)) + elif [[ ${zproblem[$x]} == "too_cold" ]]; then + ncold=$((ncold + 1)) + fi + fi + done + + globsetmode="" + globprob="" + if [[ $airconmode == "off" ]]; then + # aircon is off + if [[ $ncold -ge $limit ]]; then + globprob="too_cold" + if [[ $modelock == "n/a" || $modelock == "heat" ]]; then + globsetmode=" set_mode:heat" + fi + elif [[ $nhot -ge $limit ]]; then + globprob="too_hot" + if [[ $modelock == "n/a" || $modelock == "cool" ]]; then + globsetmode=" set_mode:cool" + fi + fi + [[ ! -z $globsetmode ]] && globaction="power_on" + else + if [[ $airconmode == "heat" && $nhot -ge 2 ]]; then + globprob="too_hot" + elif [[ $airconmode == "cool" && $ncold -ge 2 ]]; then + globprob="too_cold" + fi + [[ ! -z $globprob ]] && globaction="power_off" + fi + if [[ ! -z $globprob ]]; then + for x in ${!zname[@]}; do + if [[ ${zproblem[$x]} == $globprob && ${zaction[$x]} != *${globaction}* ]]; then + zaction[$x]="$globaction$globsetmode ${zaction[$x]}" + zactionfail[$x]=0 + elif [[ ${globaction} == "power_off" && ${zaction[$x]} != *$globaction* ]]; then + zactionfail[$x]=1 + fi + done + fi + + # pass xx - if we're closing the constant zone, and it's NOT the myzone, + # just turn the whole system off (since it will probably be opened. + # again anyway). + if [[ -z $powerchange && $airconmode != "off" ]]; then + for x in ${!zname[@]}; do + if [[ ${zaction[$x]} == *close_vent* && $constant == ${zname[$x]} ]]; then + if [[ $airconmyzoneid != ${zid[$x]} ]]; then + zaction[$x]="power_off" + [[ -z $powerchange ]] && powerchange="power_off" + fi + fi + done + fi + + # pass 3 - If we're powering the whole system on/off, don't do anything else + if [[ ! -z $powerchange ]]; then + for x in ${!zname[@]}; do + if [[ ${powerchange} == "power_off" && ${zaction[$x]} != $powerchange ]]; then + zactionfail[$x]=1 + fi + done + fi + + # pass xx - if we're closing the myzone, pick a new one +[[ $db -eq 1 ]] && info "pass 3 - powerchange is '$powerchange'" + if [[ -z $powerchange ]]; then + if [[ $airconmode != "off" ]]; then + mzmustmove=0 + nomyzone="" + for x in ${!zname[@]}; do + if [[ $airconmyzoneid == ${zid[$x]} || ${zaction[$x]} == *set_myzone* ]]; then + if [[ ${zstate[$x]} == "close" || ${zaction[$x]} == *close_vent* ]]; then + mzmustmove=1 + elif [[ ${zaction[@]} == *close:${zname[$x]}* ]]; then + mzmustmove=1 + fi + if [[ $mzmustmove -eq 1 ]]; then +[[ $db -eq 1 ]] && info "myzone must move away from ${zname[$x]}" + #info "myzone must move away from ${zname[$x]}" + zaction[$x]="${zaction[$x]/set_myzone/}" + nomyzone="$nomyzone ${zname[$x]} " +[[ $db -eq 1 ]] && info "zaction[${zname[$z]} is '${zaction[$x]}'" +[[ $db -eq 1 ]] && info "nomyzone is '$nomyzone}'" + fi + fi + done + if [[ $mzmustmove -eq 1 ]]; then +[[ $db -eq 1 ]] && info "got mzmustmove == 1" +[[ $db -eq 1 ]] && info "FINAL nomyzone is '$nomyzone}'" + # find an alternative + for x in ${!zname[@]}; do + # open or opening? set myzone. we'll resolve + # duplicates in the next step. +[[ $db -eq 1 ]] && info " checking if we can move mz to ${zname[$x]}" + poss=0 + if [[ ${zaction[$x]} != *set_myzone* && $nomyzone != *\ ${zname[$x]}\ * ]]; then +[[ $db -eq 1 ]] && info " not already moving mz here + not in nomyzone list" + if [[ ${zstate[$x]} == "open" || ${zaction[$x]} == *open_vent* ]]; then +[[ $db -eq 1 ]] && info " is open or opening -> OK" + poss=1 + elif [[ ${zaction[@]} == *open:${zname[$x]}* ]]; then +[[ $db -eq 1 ]] && info " is opening via adjacency-> OK" + poss=1 + fi + if [[ $poss -eq 1 ]]; then +[[ $db -eq 1 ]] && info " myzone could move to ${zname[$x]}" + #info " myzone could move to ${zname[$x]}" + zaction[$x]="${zaction[$x]} set_myzone" +[[ $db -eq 1 ]] && info " new zaction[${zname[$x]}] is ${zaction[x]}" + fi + fi + done + fi + fi + fi + + + # pass 4 - multiple myzones? + if [[ -z $powerchange ]]; then + nmyzones=0 + maxpri=-999 + bestidx=-1 + for x in ${!zname[@]}; do + # Multiple myzones? + if [[ ${zaction[$x]} == *set_myzone* ]]; then + #info " ${zname[$x]} wants myzone (pri=${zpri[$x]} maxpri=$maxpri)" + nmyzones=$((nmyzones + 1)) + if [[ $(echo "scale=1; ${zpri[$x]} > $maxpri" | bc) == "1" ]]; then + bestidx=$x + maxpri=$(echo "scale=1; ${zpri[$x]}" | bc) + #info " new best priority: ${zname[$x]} = $maxpri" + fi + fi + done + if [[ $nmyzones -gt 1 ]]; then + for x in ${!zname[@]}; do + if [[ $x -ne $bestidx && ${zaction[$x]} == *set_myzone* ]]; then + zaction[$x]="${zaction[$x]/set_myzone/}" + fi + done + fi + fi + + # final pass + for x in ${!zname[@]}; do + if [[ ${zactionfail[$x]} -eq 0 ]]; then + # Remove myzone if already set + if [[ ${zaction[$x]} == *set_myzone* && $airconmyzoneid == ${zid[x]} ]]; then + zaction[$x]="${zaction[$x]/set_myzone/}" + fi + + # Remove leading spaces + zaction[$x]=$(echo ${zaction[$x]} | awk '{$1=$1; sub("n/a ",""); sub(" n/a","") };1') + # Generate actual aircon commands from zone commands + gen_aircon_command ${x} ${zaction[$x]} + fi + done +} + + +# gen_aircon_command zone_idx "command1 command2 etc" +function gen_aircon_command() { + local idx allactions this toadd num doneset othername otherid otheridx db donemode str + idx=$1 + shift + allactions="$*" + doneset=0 + donemode=0 + + db=0 + [[ $db -eq 1 ]] && info "gen_aircon_command() for idx=${idx}: $allactions" + + IFS=' ' read -ra tok <<< "${allactions}" + for this in ${tok[@]}; do + if [[ $this == *power_on* ]]; then + add_aircon_command $idx -1 "Power on system" "on" + elif [[ $this == *power_off* ]]; then + add_aircon_command $idx -1 "Power off system" "off" + elif [[ $this == *set_myzone* && $airconmyzoneid != ${zid[$idx]} ]]; then + add_aircon_command $idx -1 "Set MyZone to ${zname[$idx]}" "myzone --zone ${zid[$idx]}" + elif [[ $this == *open:* ]]; then + othername=$(echo "$this" | sed -e 's/^.*open://;s/ .*//') + otheridx=$(getidxfromname $othername) + otherid=${zid[$otheridx]} + add_aircon_command $idx $otheridx "Open vent in ${zname[$otheridx]}" "set --zone ${otherid} --state on --temp ${zsettemp[$otheridx]}" + elif [[ $this == *close:* ]]; then + othername=$(echo "$this" | sed -e 's/^.*close://;s/ .*//') + otheridx=$(getidxfromname $othername) + otherid=${zid[$otheridx]} + add_aircon_command $idx $otheridx "Close vent in ${zname[$otheridx]}" "set --zone ${otherid} --state off --temp ${zsettemp[$otheridx]}" + elif [[ $this == *set_mode:* ]]; then + str=$(echo "$this" | sed -e 's/^.*set_mode://;s/ .*//') + add_aircon_command $idx -1 "Set system mode to '$str'" "$str" + elif [[ $this == *set_temp* || $this == *open_vent* || $this == *close_vent* ]]; then + if [[ $doneset -eq 0 ]]; then + toadd="" + comm="" + if [[ $allactions == *open_vent* ]]; then + toadd="$toadd --state on" + comm="open vent" + elif [[ $allactions == *close_vent* ]]; then + toadd="$toadd --state off" + comm="close vent" + fi + if [[ $allactions == *set_temp* ]]; then + num=$(echo "$this" | sed -e 's/^.*set_temp://;s/ .*//') + toadd="$toadd --temp $num" + [[ ! -z $comm ]] && comm="${comm} and " + comm="${comm}set temperature to $num degrees" + else + toadd="$toadd --temp ${zsettemp[$idx]}" + fi + if [[ ! -z $toadd ]]; then + add_aircon_command $idx -1 "In zone ${zname[$idx]}, $comm" "set --zone ${zid[$idx]}$toadd" + doneset=1 + fi + fi + fi + done +} + +# add_aircon_command zone_idx "comment goes here" "actual pymyair command to run" +function add_aircon_command() { + local x idx otheridx comment db + + [[ $# -le 1 ]] && return 1 + + idx=$1 + shift + otheridx=$1 + shift + comment=$1 + shift + + if [[ $otheridx -ne -1 ]]; then + if [[ ! -z $comment ]]; then + comment="${comment} (to fix ${zname[$idx]})" + fi + fi + + db=0 + [[ $db -eq 1 ]] && info " add_aircon_command() for idx=${idx} otheridx=${otheridx} comment=[$comment]: $*" >/dev/stderr + + [[ -z $* ]] && return 1 + + for x in ${airconcmd[@]}; do + if [[ $x == "$*" ]]; then + return 1 + fi + done + airconcmd[$nairconcommands]="$*" + airconcomment[$nairconcommands]="$comment" + airconcmdzone[$nairconcommands]="${zname[$idx]}" + airconcmdotherzone[$nairconcommands]="${zname[$otheridx]}" + airconproblem[$nairconcommands]="${zname[$idx]} is ${zproblem[$idx]/_/ }" + #if [[ $otheridx -ne -1 ]]; then + # airconcomment[$nairconcommands]="${airconcomment[$nairconcommands]}, update nearby zone ${zname[$otheridx]}" + #fi + nairconcommands=$((nairconcommands + 1)) + return 0 + +} + +function addnoop() { + local idx + idx=$(getidxfromname "$1") + zignore[$idx]=1 +} + +function addforcevent() { + local x idx + idx=$nforcevents + for x in ${!fv_zonename[@]}; do + if [[ ${fv_zonename[$x]} == "$1" ]]; then + idx=$x + break; + fi + done + fv_zonename[$idx]="$1" + fv_state[$idx]="$2" + [[ $idx == $nforcevents ]] && nforcevents=$((nforcevents + 1)) +} + +function addtemprange() { + local x idx + idx=$ntempranges + for x in ${!tr_zonename[@]}; do + if [[ ${tr_zonename[$x]} == "$1" ]]; then + idx=$x + break; + fi + done + tr_zonename[$idx]="$1" + tr_range[$idx]="$2" + [[ $idx == $ntempranges ]] && ntempranges=$((ntempranges + 1)) +} + +function addadj() { + local name + name="$1" + shift + adj_zonename[$nadj]="$name" + adj_nearby[$nadj]="$*" + nadj=$((nadj + 1)) +} + +function addperson() { + local h p x idx db + db=0 + + p="$1" + shift + h="$*" + + [[ $db -eq 1 ]] && info "addperson() person $p host(s) [$h]" + + idx=$npeople + for x in ${!pname[@]}; do + if [[ ${pname[$x]} == "$p" ]]; then + [[ $db -eq 1 ]] && info " owner already exists, appending." + idx=$x + break; + fi + done + pname[$idx]="$p" + if [[ ${pdev[$idx]} != *\ $h\ * ]]; then + pdev[$idx]="${pdev[$idx]} $h " + fi + [[ $db -eq 1 ]] && info " dev val is: ${pdev[$idx]}" + [[ $idx == $npeople ]] && npeople=$((npeople + 1)) +} + +function addowner() { + local z h local x idx db + db=0 + + z="$1" + shift + h="$*" + + [[ $db -eq 1 ]] && info "addowner() zone $z host(s) [$h]" + + idx=$nowners + for x in ${!ownerzone[@]}; do + if [[ ${ownerzone[$x]} == "$z" ]]; then + [[ $db -eq 1 ]] && info " owner already exists, appending." + idx=$x + break; + fi + done + ownerzone[$idx]="$z" + if [[ ${ownerhost[$idx]} != *\ $h\ * ]]; then + ownerhost[$idx]="${ownerhost[$idx]} $h " + fi + [[ $db -eq 1 ]] && info " hosts val is: ${ownerhost[$idx]}" + [[ $idx == $nowners ]] && nowners=$((nowners + 1)) +} + +# getownerperson zone_name +# returns name of person who owns zone +function getownerperson() { + local x retval + retval="" + for x in ${!ownerzone[@]}; do + if [[ ${ownerzone[$x]} == "$1" ]]; then + retval="${ownerperson[$x]}" + break + fi + done + echo "$retval" +} + +# getowner zone_name +# returns devices associated woth zone +function getowner() { + local x retval + retval="" + for x in ${!ownerzone[@]}; do + if [[ ${ownerzone[$x]} == "$1" ]]; then + retval="${ownerhost[$x]}" + break + fi + done + echo "$retval" +} + +# gettemprange zone_name +function gettemprange() { + local x retval + retval="n/a" + for x in ${!tr_zonename[@]}; do + if [[ ${tr_zonename[$x]} == "$1" ]]; then + retval="${tr_range[$x]}" + break + fi + done + echo "$retval" +} + +function getstrtype() { + local strtype + shopt -s nocasematch + strtype=unknown + if [[ "$*" =~ ^[0-9]{4}$ ]]; then + strtype="24h" + elif [[ "$*" =~ ^(mon|tue|wed|thu|fri|sat|sun)$ ]]; then + strtype="weekday" + fi + shopt -u nocasematch + echo "$strtype" +} + +function numtoweekday() { + local num + shopt -s nocasematch + num=-1 + case "$1" in + "mon") num=1;; + "tue") num=2;; + "wed") num=3;; + "thu") num=4;; + "fri") num=5;; + "sat") num=6;; + "sun") num=7;; + esac + shopt -u nocasematch + echo "$num" +} + +function conderror() { + error "Invalid condition: $1" + echo -e "${RED} Start: $2" + echo -e "${RED} End: $3" +} + +function load_config() { + local line rv timestr ttok stype ok starth endh nowh + local cond allconds x + local db + db=0 + rv=0 + while read line; do + line=${line%%#*} # remove comments + if [[ ! -z ${line// } ]]; then + # time based options + if [[ ${line:0:1} == "@" ]]; then + timestr=${line%% *} + timestr=${timestr:1} + IFS=';' read -ra allconds <<< "$timestr" + for cond in ${allconds[@]} ; do + [[ $db -eq 1 ]] && info "Processing condition: '$cond'" + IFS='-' read -ra ttok <<< "$cond" + if [[ ${#ttok[@]} == 1 ]]; then + stype[0]=$(getstrtype ${ttok[0]}) + if [[ ${stype[0]} != "weekday" ]]; then + error "Invalid condition: '${cond}'" + return 1 + fi + starth=$(numtoweekday ${ttok[0]}) + if [[ $starth -eq -1 ]]; then + error "'${ttok[0]}' is not a valid type here (type is ${stype[0]})" + + return 1 + fi + endh="$starth" + nowh=$(date +%u) + else + stype[0]=$(getstrtype ${ttok[0]}) + stype[1]=$(getstrtype ${ttok[1]}) + starth="" + endh="" + nowh="" + if [[ ${stype[0]} != ${stype[1]} ]]; then + error "start and end condition types don't match:" + echo -e "${RED} Start: ${ttok[0]} (${stype[0]})${PLAIN}" + echo -e "${RED} End: ${ttok[1]} (${stype[1]})${PLAIN}" + return 1 + elif [[ ${stype[0]} == "24h" ]]; then + starth=${ttok[0]} + endh=${ttok[1]} + nowh=$(date +%H%M) + elif [[ ${stype[0]} == "weekday" ]]; then + starth=$(numtoweekday ${ttok[0]}) + endh=$(numtoweekday ${ttok[1]}) + if [[ $starth -eq -1 || $endh -eq -1 ]]; then + conderror "${cond}" "'${ttok[0]}' (${stype[0]})" "'${ttok[1]}' (${stype[1]})" + + return 1 + fi + nowh=$(date +%u) + else + conderror "${cond}" "'${ttok[0]}' (${stype[0]})" "'${ttok[1]}' (${stype[1]})" + return 1 + fi + fi + if [[ ! -z $starth ]]; then + [[ $db -eq 1 ]] && info " check if $nowh is between $starth and $endh" + ok=0 + if [[ $(echo "$starth < $endh" | bc) == "1" ]]; then + if [[ $(echo "$nowh >= $starth" | bc) == "1" && $(echo "$nowh <= $endh" | bc) == "1" ]]; then + ok=1 + fi + elif [[ $(echo "$starth > $endh" | bc) == "1" ]]; then + if [[ $(echo "$nowh <= $endh" | bc) == "1" || $(echo "$nowh >= $starth" | bc) == "1" ]]; then + ok=1 + fi + else + if [[ $nowh == $endh || $nowh == $starth ]]; then + ok=1 + fi + fi + [[ $db -eq 1 && $ok -eq 0 ]] && info " $nowh isn't in range $cond" + [[ $db -eq 1 && $ok -eq 1 ]] && info " MATCH: $nowh within range $cond" + fi + if [[ $ok -eq 0 ]]; then + break + fi + done + if [[ $ok -eq 1 ]]; then + # strip condition off the front + line=${line#* } + fi + else + ok=1 + fi + if [[ $ok -eq 1 ]]; then + IFS=' ' read -ra tok <<< "$line" + if [[ ${tok[0]} == "temp" ]]; then + addtemprange ${tok[1]} ${tok[2]} + elif [[ ${tok[0]} == "constant" ]]; then + if [[ -z ${tok[1]} ]]; then + error "Missing zone name in constant command." + exit 1 + elif [[ -z $constant ]]; then + constant=${tok[1]} + else + error "Constant zone defined more than once." + exit 1 + fi + elif [[ ${tok[0]} == "adj" ]]; then + addadj ${tok[1]} ${tok[@]:2} + elif [[ ${tok[0]} == "noop" || ${tok[0]} == "ignore" ]]; then + addnoop ${tok[1]} + elif [[ ${tok[0]} == "force" ]]; then + if [[ $VALID_ZONE_STATES == *\ ${tok[2]}\ * ]]; then + addforcevent ${tok[1]} ${tok[2]} + else + error "Invalid zone state '${tok[2]}'. Valid options are: $VALID_ZONE_STATES" + exit 1 + fi + elif [[ ${tok[0]} == "owner" ]]; then + addowner ${tok[1]} ${tok[@]:2} + elif [[ ${tok[0]} == "person" ]]; then + addperson ${tok[1]} ${tok[@]:2} + elif [[ ${tok[0]} == "modelock" ]]; then + if [[ $VALID_MODES == *\ ${tok[1]}\ * ]]; then + modelock="${tok[1]}" + else + error "Invalid modelock '${tok[1]}'. Valid options are: $VALID_MODES" + exit 1 + fi + elif [[ ${tok[0]} == "test" ]]; then + info "Got test option: '${tok[@]}'" + rv=1 + fi + fi + + fi + done < "$CONFIGFILE" + return $rv +} + +# if we can ping any of the args, return ok +function canping() { + local ip db host thisrv mac + db=0 + for host in $*; do + ip=$(getent hosts $host | grep -v :) + ip=${ip%% *} + if [[ -z $ip ]]; then + thisrv=1 + else + #arp -d $ip >/dev/null 2>&1 + #ping -c1 -w1 -n -q $ip >/dev/null 2>&1 & + #sleep 0.3 + mac=$(arp -n $ip) + mac=$(echo "$mac" | egrep -v "Host|xpired" | awk '{print $2}') + [[ $mac == *:* ]] && thisrv=0 || thisrv=1 + fi + [[ $db -eq 1 ]] && info "canping() $host ($ip) is $thisrv - mac=$mac" + if [[ $thisrv -eq 0 ]]; then + return 0 + fi + done + return 1 +} + +function addzone() { + local name state settemp temp wanttemp wanttempmin wanttempmax id + name="$1" + state="$2" + settemp="$3" + temp="$4" + id="$5" + zname[$nzones]="$name" + zstate[$nzones]="$state" + zsettemp[$nzones]="$settemp" + ztemp[$nzones]="$temp" + zid[$nzones]="$id" + zwanttemp[$nzones]="n/a" + zproblem[$nzones]="n/a" + zaction[$nzones]="n/a" + zactionfail[$nzones]=0 + zowner[$nzones]=$(getowner "$name") + zownerperson[$nzones]=$(getownerperson "$name") + #info "zone $name owner is [${zowner[$nzones]}]" + zignore[$nzones]=0 + zpri[$nzones]=0 + wanttemp=$(gettemprange "$name") + if [[ $wanttemp == "n/a" ]]; then + zwanttemp[$nzones]="n/a" + zwantmin[$nzones]="-99" + zwantmax[$nzones]="99" + else + zwanttemp[$nzones]="$wanttemp" + zwantmin[$nzones]="${wanttemp%-*}" + zwantmax[$nzones]="${wanttemp#*-}" + fi + + nzones=$((nzones + 1)) +} + +function getcol() { + local col + case $1 in + "off") col="$GREY";; + "cool") col="$CYAN";; + "heat") col="$RED";; + *) col="$PLAIN";; + esac + echo $col +} + +function gen_config() { + local x + if [[ -e $CONFIGFILE ]]; then + error "$CONFIGFILE already exists" + return 1 + fi + get_aircon_info + cp /dev/null $CONFIGFILE + echo "# Specify commandline args here" >> $CONFIGFILE + echo "# options -p -t 0.3 ... " >> $CONFIGFILE + echo >> $CONFIGFILE + echo "# Don't switch to modes other than this" >> $CONFIGFILE + echo "#modelock heat|cool" >> $CONFIGFILE + echo >> $CONFIGFILE + echo "# Set temperature range to enforce" >> $CONFIGFILE + for x in ${!zname[@]}; do + echo "#temp ${zname[$x]} min_temp-max_temp" >> $CONFIGFILE + done + echo >> $CONFIGFILE + echo "# Turn off zone if we can't ARP for given hostname's IP" >> $CONFIGFILE + for x in ${!zname[@]}; do + echo "#owner ${zname[$x]} hostname.domain" >> $CONFIGFILE + done + echo >> $CONFIGFILE + echo "# Define which zones are adjacent to each other (used to close nearby" >> $CONFIGFILE + echo "# zones to help control temperature)" >> $CONFIGFILE + for x in ${!zname[@]}; do + echo "#adj ${zname[$x]} adjacent_zone1 [adjacent_zone2] ... [adjacent_zoneX]" >> $CONFIGFILE + done + echo >> $CONFIGFILE + echo "# Force a zone into a certain state" >> $CONFIGFILE + echo "#force ${zname[0]} open" >> $CONFIGFILE + echo "#force ${zname[1]} close" >> $CONFIGFILE + echo >> $CONFIGFILE + echo "# All commands except options can be restricted to certain times with:" >> $CONFIGFILE + echo "# @hhmm-hhmm Restrict between certain 24 hour times" >> $CONFIGFILE + echo "# @Mon-Wed Restrict between certain weekdays" >> $CONFIGFILE + echo "# @Mon Restrict to one weekdays only" >> $CONFIGFILE + + echo "# @Mon;hhmm-hhmm Restrict both day and time" >> $CONFIGFILE + echo "# For example:" >> $CONFIGFILE + echo "# Keep zone closed on Tuesdays and Wednesdays" >> $CONFIGFILE + echo "#@Tue-Wed force ${zname[0]} close" >> $CONFIGFILE + echo "# Override temperature range between 11pm and 5am" >> $CONFIGFILE + echo "#@2300-0500 temp ${zname[0]} 20-21" >> $CONFIGFILE + echo "# Keep zone open between 10am and 11am on Mondays" >> $CONFIGFILE + echo "#@Mon;1000-1100 force ${zname[0]} open" >> $CONFIGFILE +} + +function get_aircon_info() { + profile "query aircon" + [[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -en "${GREEN}${BOLD}>> ${PLAIN}${GREEN}Querying aircon... ${PLAIN}" + zones=$(myair $AIRCON_IP zones | jq -r '.[] | [ .name, .state, .setTemp, .measuredTemp, .number ] | @csv' | tr -d '" ') + airconmode=$(myair $AIRCON_IP mode) + nzones=0 + for line in $zones; do + IFS=',' read -ra tok <<< "$line" + addzone "${tok[0]}" "${tok[1]}" "${tok[2]}" "${tok[3]}" "${tok[4]}" + done + airconmyzoneid=$(myair $AIRCON_IP myzone) + airconmyzone=$(getnamefromid $airconmyzoneid) + [[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -e "${GREEN}${BOLD}ok${PLAIN}" + profile "query aircon" +} + +function show_aircon_status() { + local zonestr zonecol settempcol actioncol thiswantcol thisstate thisstateform + local thisformat x powercol lockcol + local FORMAT1 FORMAT2 FORMATHOT FORMATCOLD FORMATOK CLOSEDFORM OPENFORM + + FORMAT1="${BOLD}%-16s%-7s%-8s%-7s%-7s%s${PLAIN}" + FORMAT2="${UNDERLINE}${FORMAT1}${PLAIN}" + FORMATHOT="${RED}%-7s${PLAIN}" + FORMATCOLD="${CYAN}%-7s${PLAIN}" + FORMATOK="${WHITE}%-7s${PLAIN}" + CLOSEDFORM="$GREY%-8s$PLAIN" + OPENFORM="$PLAIN%-8s$PLAIN" + + powercol=$(getcol $airconmode) + lockcol=$(getcol $modelock) + printf "${BOLD}Aircon mode:${PLAIN} $powercol%s$PLAIN" $airconmode + if [[ $modelock != "n/a" ]]; then + echo -e " (locked to $lockcol$modelock$PLAIN)" + else + echo + fi + printf "${BOLD}Current myzone:${PLAIN} %s (zone %s)\n" $airconmyzone $airconmyzoneid + printf "${BOLD}Tolerance:${PLAIN} %s degrees low, %s degrees high" $tolerance_l $tolerance_h + + echo + + printf "$FORMAT1\n" "" "Valid" "" "Set" "Actual" "Proposed " + printf "$FORMAT2\n" "Zone" "Temp" "State" "Temp" "Temp" "Action " + for x in ${!zname[@]}; do + if [[ ${zproblem[$x]} == "too_hot" ]]; then + thisformat="${FORMATHOT}" + elif [[ ${zproblem[$x]} == "too_cold" ]]; then + thisformat="${FORMATCOLD}" + else + thisformat="${FORMATOK}" + fi + if [[ ${airconmode} == "off" ]]; then + thisstateform="${CLOSEDFORM}" + thisstate="" + elif [[ ${zstate[$x]} == "close" ]]; then + thisstateform="${CLOSEDFORM}" + thisstate=${zstate[$x]} + else + thisstateform="${OPENFORM}" + thisstate=${zstate[$x]} + fi + thiswant="${zwanttemp[$x]}" + if [[ $thiswant == "n/a" ]]; then + thiswantcol="$GREY" + else + thiswantcol="$PLAIN" + fi + if [[ ${zactionfail[$x]} -eq 1 || ${zaction[$x]} == "n/a" ]]; then + actioncol="$GREY" + else + actioncol="$PLAIN" + fi + + if [[ ${airconmode} == "off" ]]; then + settempcol="$GREY" + else + settempcol="$PLAIN" + fi + + if [[ $airconmyzoneid == ${zid[$x]} ]]; then + zonecol="$WHITE$BOLD" + zonestr="${zname[$x]} (M)" + else + zonecol="$PLAIN" + zonestr="${zname[$x]}" + fi + if [[ $constant == ${zname[$x]} ]]; then + zonestr="${zname[$x]} (C)" + fi + + printf "$zonecol%-16s$PLAIN$thiswantcol%-7s$PLAIN$thisstateform$settempcol%-7s$PLAIN$thisformat$actioncol%s$PLAIN\n" "${zonestr}" "${thiswant}" "${thisstate}" "${zsettemp[$x]}" "${ztemp[$x]}" "${zaction[$x]}" + done + +} + +function show_analysis() { + local count x ostr + echo -e "${UNDERLINE}Analysis:${PLAIN}" + count=0 + for x in ${!zproblem[@]}; do + if [[ ${zproblem[$x]} != "n/a" ]]; then + if [[ ${zproblem[$x]} == "owner_not_home" ]]; then + if [[ ${zowner[$x]} == *\ * ]]; then + ostr="owners are" + else + ostr="owner is" + fi + echo "- ${zname[$x]} open but $ostr not detected (${zowner[$x]})." + elif [[ ${zproblem[$x]} =~ force_ ]]; then + what=${zproblem[$x]##*_} + [[ $what == "close" ]] && what="${what}d" + echo "- ${zname[$x]} is forced $what" + else + if [[ $constant == ${zname[$x]} ]]; then + append=" (constant zone)" + else + append="" + fi + echo "- ${zname[$x]} is ${zproblem[$x]/_/ }${append}" + fi + count=$((count + 1)) + fi + done + if [[ $globprob == too_* ]]; then + echo "- $limit or more zones are ${globprob/_/ }" + fi + [[ $count -eq 0 ]] && echo -e "${GREEN}All is good!${PLAIN}" +} + +function show_proposed_commands() { + local str x maxlen count cmdformat fullcmd + str="Proposed commands" + echo -e "${UNDERLINE}${str}:${PLAIN}" + + if [[ $nairconcommands -gt 0 ]]; then + if [[ -f ${KILLFILE} ]]; then + echo -e "${RED}Not running commands because killfile exists ($KILLFILE)${PLAIN}" + elif [[ $DOIT -ne 1 ]]; then + echo -e "${RED}Not running commands because -y not specified${PLAIN}" + fi + fi + + maxlen=-99 + for x in ${!airconcmd[@]}; do + fullcmd="myair $AIRCON_IP ${airconcmd[$x]}" + thislen=${#fullcmd} + [[ $thislen -gt $maxlen ]] && maxlen=$thislen + done + + cmdformat="%-$((maxlen + 3))s" + count=0 + for x in ${!airconcmd[@]}; do + fullcmd="myair $AIRCON_IP ${airconcmd[$x]}" + printf -- "$cmdformat" "$fullcmd" + [[ ! -z ${airconcomment[$x]} ]] && printf " $GREEN$BOLD# $PLAIN$GREEN%s$PLAIN\n" "${airconcomment[$x]}" || echo + count=$((count + 1)) + done + [[ $count -eq 0 ]] && echo -e "${GREY}n/a${PLAIN}" +} + +function run_commands() { + local x + for x in ${!airconcmd[@]}; do + [[ ! -z ${airconcomment[$x]} ]] && action "${airconcomment[$x]}" + echo RUNNING myair $AIRCON_IP ${airconcmd[$x]} + myair $AIRCON_IP ${airconcmd[$x]} >/dev/null 2>&1 + influx_insert "INSERT aircon action=\"${airconcomment[$x]}\",comment=\"${airconproblem[$x]}\"" + done +} + +function profile() { + local diff + [[ $profiler != 1 ]] && return + p_last=$p_now + p_now=$(date +%s) + diff=$(( p_now - $p_last)) + if [[ -z $p_str ]]; then + p_str="$*" + else + info "+${diff} secs [$*]" >/dev/stderr + p_str="" + fi + +} + +# poplulates glpbals: +# failedcmds +# influxrv +function influx_clear() { + failedcmds="" + influxrv=0 +} + +# poplulates glpbals: +# failedcmds +# influxrv +function influx_insert() { # $1 == cmd + local cmd thisrv + if [[ -z ${influx} ]]; then + return 1 + fi + cmd="$1" + if [[ $DOIT -eq 1 ]]; then + ${influx} -database "$influxdb" -execute "$cmd" + else + echo "${influx} -database $influxdb -execute $cmd" + thisrv=0 + fi + thisrv=$? + influxrv=$((influxrv + $thisrv)) + if [[ $thisrv -ne 0 ]]; then + failedcmds="$failedcmds$cmd\n" + fi +} + +# poplulates glpbals: +# influx +# influxdb +function influx_init() { # $1 == dbname + local dbfound + influx=$(which influx 2>/dev/null) + influxdb="$1" + if [[ $? -ne 0 ]]; then + error "influx executable not found in path." + return 1 + fi + dbfound=$(${influx} -database "$influxdb" -execute "SHOW DATABASES" | grep -w ^${influxdb}) + if [[ -z $dbfound ]]; then + error "Couldn't find influx database '$influxdb'" + return 1 + fi + influx_clear + return 0 +} + + + +p_now=$(date +%s) +p_last=$p_now +p_str="" + +CONFIGFILE=${DEFAULT_CONFIGFILE} +KILLFILE=${DEFAULT_KILLFILE} +AIRCON_IP=${DEFAULT_AIRCON_IP} + +airconmode="unknown" +airconmyzoneid="n/a" +airconmyzone="n/a" +globprob="" +nairconcommands=0 +ntempranges=0 +nadj=0 +nowners=0 +npeople=0 +tolerance_l=${DEFAULT_TOLERANCE} +tolerance_h=${DEFAULT_TOLERANCE} +constant="" + +DOIT=0 +makeconfig=0 +modelock="n/a" +cronmode=0 +logmode=0 #0=none, 1=csv_file, 2=local influxdb +influxdb="" +showwho=0 +limit=$DEFAULTLIMIT +profiler=0 +csvfile="$DEFAULT_CSVFILE" + + +# check for config file option first +if [[ $* == *-f\ * ]]; then + cf=$(echo "$*" | sed -e 's/.*-f //;s/ .*//') + if [[ ! -z $cf ]]; then + CONFIGFILE="$cf" + fi +fi + +ALLARGS="" +if [[ -e $CONFIGFILE ]]; then + # load options from file... + ALLARGS=$(egrep "^options " $CONFIGFILE | sed -e 's/^options //' | tr '\n' ' ') +fi + +ALLARGS="$ALLARGS $*" + +optstring="A:cf:hi:I:k:l:Lo:pymwt:T:x:" +while getopts "$optstring" i $ALLARGS; do + case "$i" in + h) + usage; + exit 1; + ;; + f) + CONFIGFILE=${OPTARG} + ;; + i) + AIRCON_IP=${OPTARG} + ;; + I) + logmode=2 + influx_init "$OPTARG" || exit 1 + ;; + A) + influx_init "$OPTARG" || exit 1 + ;; + k) + KILLFILE=${OPTARG} + ;; + l) + limit=${OPTARG} + ;; + o) + csvfile="$OPTARG" + ;; + L) + logmode=1 + ;; + t) + tolerance_l=${OPTARG} + ;; + T) + tolerance_h=${OPTARG} + ;; + c) + enable_cronmode + ;; + p) + profiler=1 + info "Profiler mode enabled" + ;; + m) + makeconfig=1 + ;; + y) + DOIT=1 + ;; + w) + showwho=1 + ;; + x) + MYAIR="${OPTARG}" + ;; + *) + usage; + exit 1; + ;; + esac +done +shift $((OPTIND - 1)) + + +if [[ -z $MYAIR ]]; then + MYAIR=$(which myair) + if [[ $? -ne 0 ]]; then + error "Can't find pymyair executable 'myair' in path. Install it from here: https://github.com/smallsam/pymyair" + exit 1 + fi +else + if [[ ! -x "$MYAIR" ]]; then + error "Specified pymyair executable '$MYAIR' not found." + exit 1 + fi +fi + +if [[ $makeconfig -eq 1 ]]; then + gen_config + rv=$? + if [[ $rv -eq 0 ]]; then + info "A config file for your current aircon setup has been generated here:" + info " $CONFIGFILE" + info "Please review this and update as required." + fi + exit $rv +fi + +if [[ -e $CONFIGFILE ]]; then + if ! load_config; then + error "Config load failed" + exit 1 + fi +else + error "Config file $CONFIGFILE doesn't exist." + exit 1 +fi + +# ping all hosts in background to populate arp table +for x in ${devices}; do + ping -c1 -w1 -n -q $ip >/dev/null 2>&1 & +done +wait + +if [[ $showwho -eq 1 ]]; then + # get max phone name length + maxlen=1 + for x in ${pname[@]}; do + [[ ${#x} -gt $maxlen ]] && maxlen=$((${#x} + 3)) + done + # get a list of all phones + TFORMAT="${BOLD}${UNDERLINE}%-${maxlen}s%-16s${PLAIN}\n" + HFORMAT="%-${maxlen}s${GREEN}%-16s${PLAIN}\n" + AFORMAT="%-${maxlen}s${RED}%-16s${PLAIN}\n" + echo + printf "$TFORMAT" "Person" "Availability" + for x in ${!pname[@]}; do + if canping "${pdev[$x]}"; then + thisform="$HFORMAT" + str="At home" + else + thisform="$AFORMAT" + str="Out of the house" + fi + printf "$thisform" "${pname[$x]}" "$str" + done + echo + exit 0 +fi + + + +get_aircon_info + +if [[ $logmode -eq 1 ]]; then + now=$(date +'%d/%m/%Y %H:%M:%S') + if [[ ! -e $csvfile ]]; then + cp /dev/null $csvfile + echo -n "Date" >>$csvfile + for x in ${!zname[@]}; do + echo -n ",${zname[$x]}" >> $csvfile + done + echo >> $csvfile + fi + echo -n "${now}" >>$csvfile + for x in ${!zname[@]}; do + echo -n ",${ztemp[$x]}" >> $csvfile + done + echo >> $csvfile + exit 0 +elif [[ $logmode -eq 2 ]]; then + now=$(date +'%d/%m/%Y %H:%M:%S') + + rv=0 + failedcmds="" + influx_clear + for x in ${!zname[@]}; do + influx_insert "INSERT temperature,room=${zname[$x]} value=${ztemp[$x]}" + done + + for x in ${!pname[@]}; do + canping "${pdev[$x]}" && ishome=1 || ishome=0 + influx_insert "INSERT attendance,person=${pname[$x]} value=$ishome" + done + [[ $airconmode == "off" ]] && pw=0 || pw=1 + influx_insert "INSERT aircon running=$pw" + # 0= off + # 1=cool + # 2=heat + # 3=dry + # 4=fan/other + case "$airconmode" in + "off") modenum=0;; + "cool") modenum=1;; + "heat") modenum=2;; + "dry") modenum=3;; + *) modenum=4;; + esac + influx_insert "INSERT aircon mode=$modenum" + + if [[ $influxrv -gt 0 ]]; then + echo -e "${RED}$failedcmds${PLAIN}" | sed -e 's/^/ /' + fi + exit ${rv} +fi + +generate_actions + +if [[ $cronmode -eq 1 ]]; then + # only show output if we are doing something + [[ $nairconcommands -ge 1 ]] && showoutput=1 || showoutput=0 +else + showoutput=1 +fi + + +if [[ $showoutput -eq 1 ]]; then + echo + show_aircon_status + echo + show_analysis + echo + show_proposed_commands + echo +fi + +# Actually run the commands +if [[ $DOIT -eq 1 ]]; then + if [[ ! -f ${KILLFILE} ]]; then + run_commands + [[ $cronmode -ne 1 ]] && echo + fi +fi