#!/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" STRIKE="\033[9m" UNDERLINE="\033[4m" DARKGREY="\033[38;2;90;90;90m" RED="\033[31m" ORANGE="\033[38;2;255;165;0m" PINK="\033[38;2;255;151;198m" MAGENTA="\033[35m" GREEN="\033[32m" YELLOW="\033[33m" BLUE="\033[34m" CYAN="\033[36m" GREY="\033[2;37m" 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 " -D dbhost Specify influxdb hostname for -I and -A options (default: localhost)" 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 " -s Validate config file then exit." echo " -S[H] Show configured rules in human-readable format" echo " (HTML if -H given, otherwise ANSI)." echo " -w List which people are available then exit." echo " -W List zone-owning 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 } # get_perfect_tolerance zone_idx function get_perfect_tolerance() { # outputs the difference from midpoint inside which temperature is considered "perfect" local min max min=${zwantmin[$idx]} max=${zwantmax[$idx]} echo "scale=2; ($max - $min) / 4" | 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 thisperfect=0 idx=$1 profile "getzoneaction for idx ${zname[$idx]}" thisaction="n/a" if [[ ${zignore[$idx]} -eq 1 ]]; then problem="n/a" thisperfect=1 else if [[ -z ${zowner[$idx]} ]]; then pingok=1 elif canping ${zowner[$idx]}; then pingok=1 else pingok=0 thisperfect=1 fi fv=$(getforcevent ${zname[$idx]}) if [[ $robtest -eq 1 ]]; then echo "canping zone ${zname[$idx]} is: $pingok" fi donearby=0 if [[ -n $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 if [[ $pingok -eq 1 ]]; then # check for 'perfect' zones if [[ ${zwanttemp[$idx]} != *-* ]]; then thisperfect=1 else local mid midtol mid=$(get_midrange $idx) midtol=$(get_perfect_tolerance $idx) #echo "zone ${zname[$idx]} temp ${ztemp[$idx]} mid is $mid midtol is $midtol perfect is >= $(echo "$mid - $midtol" | bc)" if [[ $airconmode == "heat" ]]; then thisperfect=$(echo "${ztemp[$idx]} >= (${mid} - ${midtol})" | bc) elif [[ $airconmode == "cool" ]]; then thisperfect=$(echo "${ztemp[$idx]} <= (${mid} + ${midtol})" | bc) fi fi fi fi zproblem[$idx]="$problem" zpri[$idx]="$priority" zownerhome[$idx]="$pingok" 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" zperfect[$idx]=$thisperfect profile "getzoneaction for idx ${zname[$idx]}" } function getidxfromname() { # name local x #echo "==== getidxfromname($1)" >/dev/stderr for x in ${!zname[@]}; do #echo "==== idx $x comparing zname ('${zname[$x]}') to \$1 ('$1')" >/dev/stderr if [[ ${zname[$x]} == "$1" ]]; then #echo " ==== matched. returning ${x}" >/dev/stderr echo "${x}" return fi done echo "-1" } 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 all_zones_perfect() { local x for x in ${zperfect[@]}; do if [[ $x -ne 1 ]]; then return 1 fi done return 0 } function generate_actions() { # populates global: nairconcommands & airconcmd[] & globprob 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 #echo "zoneignore for ${zname[$x]} is ${zignore[$x]}" >/dev/stderr 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 [[ -n $globsetmode ]] && globaction="power_on" else if [[ $airconmode == "heat" && $nhot -ge $limit ]]; then globprob="too_hot" elif [[ $airconmode == "cool" && $ncold -ge $limit ]]; then globprob="too_cold" elif [[ $ncold -eq 0 && $nhot -eq 0 ]]; then if all_zones_perfect; then globprob="not_needed" fi fi if [[ -n $globprob ]]; then globaction="power_off" [[ -z $powerchange ]] && powerchange="power_off" fi fi if [[ -n $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 [[ -n $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 # get list of all zones which can't be myzone nomyzone="" for x in ${!zname[@]}; do if [[ ${znomyzone[$x]} -eq 1 ]]; then nomyzone="$nomyzone ${zname[$x]} " fi done 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 elif [[ ${znomyzone[$x]} -eq 1 ]]; 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 if [[ -n $globaction ]]; then gen_aircon_command -1 ${globaction} fi } # 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 [[ $idx -eq -1 ]] && continue add_aircon_command $idx -1 "Set MyZone to ${zname[$idx]}" "myzone --zone ${zid[$idx]}" elif [[ $this == *open:* ]]; then [[ $idx -eq -1 ]] && continue 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 [[ $idx -eq -1 ]] && continue 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 [[ $idx -eq -1 ]] && continue 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" [[ -n $comm ]] && comm="${comm} and " comm="${comm}set temperature to $num degrees" else toadd="$toadd --temp ${zsettemp[$idx]}" fi if [[ -n $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 [[ -n $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 # already got this command queued? for x in ${airconcmd[@]}; do if [[ $x == "$*" ]]; then return 1 fi done airconcmd[$nairconcommands]="$*" airconcomment[$nairconcommands]="$comment" if [[ $idx -eq -1 ]]; then airconcmdzone[$nairconcommands]="Aircon" airconproblem[$nairconcommands]="Aircon is not needed at this time." else airconcmdzone[$nairconcommands]="${zname[$idx]}" airconproblem[$nairconcommands]="${zname[$idx]} is ${zproblem[$idx]/_/ }" fi if [[ $otheridx -eq -1 ]]; then airconcmdotherzone[$nairconcommands]="" else airconcmdotherzone[$nairconcommands]="${zname[$otheridx]}" fi #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 addnomyzone() { local idx idx=$(getidxfromname "$1") znomyzone[$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() { # [-s] idx min-max -s means 'set zwanttemp+zwantmin+zwantmax too' local x idx doset=0 if [[ $1 == "-s" ]]; then doset=1 shift fi 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" if [[ $doset -eq 1 ]]; then local wanttemp zidx wanttemp=$(gettemprange "$1") zidx=$(getidxfromname "$1") if [[ $wanttemp == "n/a" ]]; then zwanttemp[$zidx]="n/a" zwantmin[$zidx]="-99" zwantmax[$zidx]="99" else zwanttemp[$zidx]="$wanttemp" zwantmin[$zidx]="${wanttemp%-*}" zwantmax[$zidx]="${wanttemp#*-}" fi fi [[ $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() { # zone "host1 host2 ..." local z h local x idx db doset=0 local zidx db=0 if [[ $1 == "-s" ]]; then doset=1 shift fi z="$1" shift h="$*" zidx=$(getidxfromname "$z") # get the index into the zname[] etc arrays if [[ $zidx -eq -1 ]]; then error "Failed to set owner of zone '$z' to '$h' - zone doesn't exist. " exit 1; fi [[ $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)) if [[ $doset -eq 1 ]]; then zowner[$zidx]="${ownerhost[$idx]}"; [[ $db -eq 1 ]] && info " doing set of zowner[$zidx] (${zname[$zidx]}} to [${ownerhost[$idx]}]" #zownerperson[$zidx]=$(getownerperson "$z"); # may be overwritten later fi } # 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 with 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 weekdaytonum() { 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 parse_config || return 1 db=0 rv=0 while read line; do ok=1 # default to okay line=${line%%#*} # remove comments if [[ -n ${line// } ]]; then # time based options if [[ ${line:0:1} == "@" ]]; then processtimeconditions "$line" || ok=0 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 -s ${tok[1]} ${tok[2]} elif [[ ${tok[0]} == "constant" ]]; then constant=${tok[1]} elif [[ ${tok[0]} == "adj" ]]; then addadj ${tok[1]} ${tok[@]:2} elif [[ ${tok[0]} == "nomyzone" || ${tok[0]} == "nomy" ]]; then addnomyzone ${tok[1]} elif [[ ${tok[0]} == "noop" || ${tok[0]} == "ignore" ]]; then addnoop ${tok[1]} elif [[ ${tok[0]} == "force" ]]; then addforcevent ${tok[1]} ${tok[2]} elif [[ ${tok[0]} == "owner" ]]; then addowner -s ${tok[1]} ${tok[@]:2} elif [[ ${tok[0]} == "person" ]]; then addperson ${tok[1]} ${tok[@]:2} elif [[ ${tok[0]} == "modelock" ]]; then modelock="${tok[1]}" elif [[ ${tok[0]} == "test" ]]; then info "Got test option: '${tok[@]}'" rv=1 fi fi fi done < "$CONFIGFILE" return $rv } # pooulates global timestr_human, -c means keep going if we dont match function processtimeconditions() { local timestr line allconds cond local ttok stype nowh starth endh local this_human local keepgoing=0 local ok=0 local nonmatches=0 local rv if [[ $1 == "-c" ]]; then keepgoing=1 shift 1 fi line="$*" timestr_human="" 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=$(weekdaytonum ${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) [[ -n $timestr_human ]] && this_human=" b" || this_human=B this_human=$(printf "${this_human}etween _H_%02s:%02s_EH_ and _H_%02s:%02s_EH_" ${starth:0:2} ${starth:2:2} ${endh:0:2} ${endh:2:2}) elif [[ ${stype[0]} == "weekday" ]]; then starth=$(weekdaytonum ${ttok[0]}) endh=$(weekdaytonum ${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) if [[ ${ttok[1]} == ${ttok[2]} ]]; then [[ -n $timestr_human ]] && this_human="o " || this_human=O this_human=$(printf "${this_human}n _H_%ss_EH_" ${ttok[0]}) else [[ -n $timestr_human ]] && this_human=" f" || this_human=F this_human=$(printf "${this_human}rom _H_%s_EH_ to _H_%s_EH_" ${ttok[0]} ${ttok[1]}) fi else conderror "${cond}" "'${ttok[0]}' (${stype[0]})" "'${ttok[1]}' (${stype[1]})" return 1 fi fi if [[ -n $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 [[ $ok -eq 0 ]] && nonmatches=$((nonmatches + 1)) [[ $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 timestr_human="${timestr_human}${this_human}" if [[ $ok -eq 0 && $keepgoing -eq 0 ]]; then break fi done [[ $db -eq 1 ]] && info " ok=$ok nonmatches=$nonmatches" if [[ $ok -eq 0 || $nonmatches -gt 0 ]]; then rv=1 else rv=0 fi [[ $db -eq 1 ]] && info " returning $rv" return $rv } function active_cols() { if [[ $RULEFORMAT == "html" ]]; then plainc="" # black inactc="" # red linec="" # black devc="" # green+bold devbc="" # green statec="" # yellow+bold roomc="" # orange timec="" # pink timebc="" # pink+bold minc="" # blue+bold maxc="" # red+bold coolc="" # cyan+bold heatc="" # red+bold nl="
\n" else plainc=${PLAIN} inactc="$RED" linec=${PLAIN} devc="$GREEN" devbc="$BOLD$devc" statec="$BOLD$YELLOW" roomc="$ORANGE" timec="$PINK" timebc="$BOLD$timec" minc="$BOLD$BLUE" maxc="$BOLD$RED" coolc="$BOLD$CYAN" heatc="$RED" nl="\n" fi } function inactive_cols() { if [[ $RULEFORMAT == "html" ]]; then plainc="
" # black inactc="" # lightgrey linec="" devc="" devbc="" statec="" roomc="" timec="" timebc="" minc="" maxc="" nl="
\n" else plainc=${PLAIN} inactc="$DARKGREY" linec="${inactc}" devc="$inactc" devbc="$inactc" statec="$inactc" roomc="$inactc" timec="$inactc" timebc="$inactc" minc="$inactc" maxc="$inactc" nl="\n" fi } function parse_config() { local line rv timestr ttok stype ok starth endh nowh local cond allconds x local db fileok=1 linenum show=0 local config_human line_human errstr local ign="" thisignored=0 oneof if [[ -n $1 ]]; then show=$1 fi db=0 rv=0 config_human="" errstr="" linenum=1 while read line; do ok=1 active_cols line=${line%%#*} # remove comments thisignored=0 line_human="" if [[ -n ${line// } ]]; then # time based options if [[ ${line:0:1} == "@" ]]; then # strip condition off the front processtimeconditions -c "$line" || ok=0 [[ $ok -eq 0 ]] && inactive_cols line=${line#* } if [[ -n $timestr_human ]]; then local modts modts=${timestr_human//_H_/$timebc} modts=${modts//_EH_/$linec$timec} line_human="${timec}$modts${linec}, " fi fi IFS=' ' read -ra tok <<< "$line" if [[ ${tok[0]} == "temp" ]]; then local min max min=${tok[2]%-*} max=${tok[2]#*-} [[ $ign == *\ ${tok[1]}* ]] && thisignored=1 # zone is being ignored [[ $thisignored -eq 1 ]] && inactive_cols line_human="${line_human}Keep temperature of ${roomc}${tok[1]}${linec} between ${minc}${min}${linec}-${maxc}${max}${linec} degrees" elif [[ ${tok[0]} == "constant" ]]; then true elif [[ ${tok[0]} == "person" ]]; then true elif [[ ${tok[0]} == "adj" ]]; then true elif [[ ${tok[0]} == "nomyzone" || ${tok[0]} == "nomy" ]]; then line_human="${line_human}Prevent ${roomc}${tok[1]}${linec} from being the MyZone" elif [[ ${tok[0]} == "noop" || ${tok[0]} == "ignore" ]]; then line_human="${line_human}Ignore ${roomc}${tok[1]}${linec} temperature when deciding what to do" ign="${ign} ${tok[1]}" elif [[ ${tok[0]} == "force" ]]; then if [[ $VALID_ZONE_STATES == *\ ${tok[2]}\ * ]]; then local adj adj="${tok[2]}" [[ $adj != "open" ]] && adj="${adj}d" line_human="${line_human}Force the vent in ${roomc}${tok[1]}${linec} to be ${statec}${adj}${linec}" else errstr="${errstr}${linenum}:Invalid zone state '${tok[2]}'. Valid options are: $VALID_ZONE_STATES$nl" fileok=0 fi elif [[ ${tok[0]} == "owner" ]]; then local devices verb devices="${devc}${tok[@]:2}${linec}" if [[ ${#tok[@]} -eq 3 ]]; then verb="isn't" oneof="" devices="${devbc}${linec}${devices}${devbc}${linec}" else verb="aren't" oneof=" one of" devices="${devbc}(${linec}${devices}${devbc})${linec}" fi line_human="${line_human}${statec}Close${linec} vent in ${roomc}${tok[1]}${linec} if${oneof} $devices $verb online" elif [[ ${tok[0]} == "modelock" ]]; then if [[ $VALID_MODES == *\ ${tok[1]}\ * ]]; then local col modelock="${tok[1]}" [[ $modelock == "cool" ]] && col="$coolc" || col="$heatc" line_human="${line_human}Only operate the aircon in ${col}${tok[1]}${linec} mode" else errstr="${errstr}${linenum}:Invalid modelock '${tok[1]}'. Valid options are: $VALID_MODES$nl" fileok=0 fi elif [[ ${tok[0]} == "test" ]]; then line_human="${line_human}Got test option '${roomc}${tok[1]}${linec}'" elif [[ -n $line ]]; then errstr="${errstr}${linenum}:Syntax error: ${line}$nl" fileok=0 fi fi if [[ -n $line_human ]]; then if [[ $ok -eq 0 ]]; then line_human="${plainc}${inactc}[out-of-hours] ${linec}${line_human}" elif [[ $thisignored -eq 1 ]]; then line_human="${plainc}${inactc}[ignored] ${linec}${line_human}" fi config_human="${config_human}- ${linec}${line_human}${plainc}$nl" fi linenum=$((linenum + 1)) done < "$CONFIGFILE" if [[ $fileok -eq 1 ]]; then if [[ $show -eq 2 ]]; then echo -e "${config_human}" elif [[ $show -eq 1 ]]; then info "Configuration file is ${BOLD}OK" fi rv=0 else error "Configuration file is invalid:" echo -e "${RED}$errstr${plainc}" | sed -e 's/^/ /' rv=1 fi return $rv } # if we can ping any of the args, return ok function canping() { local ip db host thisrv mac local os os=$(uname -s) db=0 for host in $*; do if [[ $os == "Darwin" ]]; then ip=$(dscacheutil -q host -a name $host | grep ^ip_address: | awk '{ print $NF}') else ip=$(getent hosts $host | grep -v :) fi 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 & arping -c10 -W0.1 -w3 -C1 -q ${ip} 2>/dev/null 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" zperfect[$nzones]=0 zproblem[$nzones]="n/a" zaction[$nzones]="n/a" zactionfail[$nzones]=0 zowner[$nzones]=$(getowner "$name") # devices which 'own' the zone - may be overwritten later #zownerperson[$nzones]=$(getownerperson "$name") # may be overwritten later zownerhome[$nzones]=1 #info "zone $name owner is [${zowner[$nzones]}]" zignore[$nzones]=0 znomyzone[$nzones]=0 zpri[$nzones]=0 wanttemp=$(gettemprange "$name") if [[ $wanttemp == "n/a" ]]; then zwanttemp[$nzones]="n/a" # default - this will be overridden in load_config zwantmin[$nzones]="-99" # default - this will be overridden in load_config zwantmax[$nzones]="99" # default - this will be overridden in load_config 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 actualtempformat x powercol lockcol actionstr local FORMAT1 FORMAT2 FORMATHOT FORMATCOLD FORMATOK CLOSEDFORM OPENFORM local ownercol ownerform FORMAT1="${BOLD}%-16s%-9s%-8s%-7s%-7s%-7s%s${PLAIN}" FORMAT2="${UNDERLINE}${FORMAT1}${PLAIN}" FORMATHOT="${RED}%-7s${PLAIN}" FORMATCOLD="${CYAN}%-7s${PLAIN}" FORMATOK="${WHITE}%-7s${PLAIN}" FORMATPERFECT="${GREEN}%-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" "Owner" "Proposed " printf "$FORMAT2\n" "Zone" "Temp" "State" "Temp" "Temp" "Home" "Action " for x in ${!zname[@]}; do if [[ ${zproblem[$x]} == "too_hot" ]]; then actualtempformat="${FORMATHOT}" elif [[ ${zproblem[$x]} == "too_cold" ]]; then actualtempformat="${FORMATCOLD}" elif [[ ${zperfect[$x]} -eq 1 ]]; then actualtempformat="${FORMATPERFECT}" else actualtempformat="${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 # override if [[ ${zignore[$x]} -eq 1 ]]; then thiswant="ignore" thiswantcol="$GREY" 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 if [[ -z ${zowner[$x]} ]]; then ownercol="$GREY" ownerstr="n/a" elif [[ ${zownerhome[$x]} -eq 1 ]]; then ownercol="$PLAIN" ownerstr="yes" else ownercol="$GREY" ownerstr="no" fi actionstr="${zaction[$x]}" #if [[ ${zaction[$x]} == "n/a" && ${zignore[$x]} -eq 1 ]]; then # actionstr="${actionstr} (zone ignored)" #fi printf "$zonecol%-16s$PLAIN$thiswantcol%-9s$PLAIN$thisstateform$settempcol%-7s$PLAIN${actualtempformat}${ownercol}%-7s${PLAIN}$actioncol%s$PLAIN\n" "${zonestr}" "${thiswant}" "${thisstate}" "${zsettemp[$x]}" "${ztemp[$x]}" "$ownerstr" "${actionstr}" 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 [[ ${zownerhome[$x]} -ne 1 ]]; then if [[ ${zowner[$x]} == *\ * ]]; then append1=", but (${zowner[$x]}) aren't online" else append1=", but ${zowner[$x]} isn't online" fi else append1="" fi if [[ $constant == ${zname[$x]} ]]; then append2=" (constant zone)" else append2="" fi echo "- ${zname[$x]} is ${zproblem[$x]/_/ }${append1}${append2}" fi count=$((count + 1)) fi done if [[ $globprob == too_* ]]; then echo "- $limit or more zones are ${globprob/_/ }" count=$((count + 1)) elif [[ $globprob == "not_needed" ]]; then echo "- System is on but all zones at optimal temperature" count=$((count + 1)) 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" [[ -n ${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 [[ -n ${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} -host "$influxhost" -database "$influxdb" -execute "$cmd" else echo "${influx} -host "$influxhost" -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} -host "$influxhost" -database "$influxdb" -execute "SHOW DATABASES" | grep -w ^${influxdb}) if [[ -z $dbfound ]]; then error "Couldn't find influx database '$influxdb' on host $influxhost" 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="" influxhost="127.0.0.1" showwho=0 # 1 = show people, 2 = show owners limit=$DEFAULTLIMIT profiler=0 csvfile="$DEFAULT_CSVFILE" sanitycheck=0 RULEFORMAT=ansi robtest=0 # check for config file option first if [[ $* == *-f\ * ]]; then cf=$(echo "$*" | sed -e 's/.*-f //;s/ .*//') if [[ -n $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:cD:f:hHi:I:k:l:Lo:pmRsSt:T:wWx:y" while getopts "$optstring" i $ALLARGS; do case "$i" in h) usage; exit 1; ;; H) RULEFORMAT=html ;; f) CONFIGFILE=${OPTARG} ;; i) AIRCON_IP=${OPTARG} ;; D) influxhost="${OPTARG}" ;; I) logmode=2 influxdb="$OPTARG" ;; A) influxdb="$OPTARG" ;; k) KILLFILE=${OPTARG} ;; l) limit=${OPTARG} ;; o) csvfile="$OPTARG" ;; L) logmode=1 ;; t) tolerance_l=${OPTARG} ;; T) tolerance_h=${OPTARG} ;; R) robtest=1 ;; c) enable_cronmode ;; p) profiler=1 info "Profiler mode enabled" ;; m) makeconfig=1 ;; s) sanitycheck=1 ;; S) sanitycheck=2 ;; y) DOIT=1 ;; w) showwho=1 ;; W) showwho=2 ;; x) MYAIR="${OPTARG}" ;; *) usage; exit 1; ;; esac done shift $((OPTIND - 1)) if [[ -n $influxdb ]]; then influx_init "$influxdb" || exit 1 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 [[ $showwho -eq 0 ]]; then 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 fi # Must do this BEFORE parsing the config file, otherwise # we can't resolve zone names. get_aircon_info if [[ -e $CONFIGFILE ]]; then if [[ $sanitycheck -ge 1 ]]; then parse_config $sanitycheck exit $? elif ! 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 [[ $robtest -eq 1 ]]; then for x in ${!zname[@]}; do echo "zone ${zname[$x]} owner is [${zowner[$x]}]" done exit 1 fi 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 elif [[ $showwho -eq 2 ]]; then # get max device hostname length maxlen=1 for x in ${ownerhost[@]}; do [[ ${#x} -gt $maxlen ]] && maxlen=$((${#x} + 3)) done # get a list of all devices TFORMAT="${BOLD}${UNDERLINE}%-${maxlen}s%-16s%s${PLAIN}\n" HFORMAT="%-${maxlen}s${GREEN}%-16s${PLAIN}%s\n" AFORMAT="%-${maxlen}s${RED}%-16s${PLAIN}%s\n" alldevs=$(echo "${ownerhost[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ') echo printf "$TFORMAT" "Devices" "Availability" "Zones owned" for x in $alldevs; do if canping ${x}; then thisform="$HFORMAT" str="Available" else thisform="$AFORMAT" str="Unavailable" fi unset mydevs || declare -a mydevs for i in ${!ownerzone[@]}; do if [[ ${ownerhost[$i]} == *${x}* ]]; then mydevs+=("${ownerzone[$i]}") fi done allmydevs=$(echo "${mydevs[@]}" | tr '\n' ' ') printf "$thisform" "${x}" "$str" "${allmydevs}" done echo exit 0 fi 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