aircon/aircon.sh

2440 lines
80 KiB
Bash
Executable File

#!/bin/bash
# TODO: comparison to previous reading
# TODO: gnuplot of given time range
# 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 "
ARPING_DEF=/usr/local/sbin/arping
ARPING=${ARPING_DEF}
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"
RULEDB=0
export PYTHONWARNINGS="ignore" # for urllib NotOpenSSLWarning
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 " -a file Specify location of arping binary (default: $ARPING_DEF)."
echo " -b Rule debug mode. Show processing of time rules."
echo " -A db Log actions to given influxdb database."
echo " -c Cron mode. Only show output if actions were taken."
echo " -D dbhost Specify influxdb hostname for -I and -A options (default: localhost)"
echo " -e file After removing expired killfile, write exptext (see -E) to this file."
echo " -E exptext Define 'exptext' for -e option."
echo " -g file Send Tele(g)ram notifications via api.telegran.org. File format is:"
echo " bottoken"
echo " chatid"
echo " -f file Specify an alternate config file."
echo " -F zonename:temp Forcibly provide current zone temperature (for testing)"
echo " -h Show this text."
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 " -k file If file exists and is empty, never change aircon settings (default: $DEFAULT_KILLFILE)."
echo " If file is non-empty, never change aircon settings if current unixtime < file contents."
echo " File will be removed if current unixtime >= file contents."
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 " -m Generate a config file based on current aircon setup."
echo " -o file Specify CSV output file. Default: $DEFAULT_CSVFILE"
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 " -t num Specify degrees below min temperature before taking action."
echo " -T num Specify degrees above max temperature before taking action."
echo " -w List which people are available then exit."
echo " -W List zone-owning devices are available then exit."
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 warn() {
echo -e "$BOLD${YELLOW}Warning: $PLAIN$YELLOW$*$PLAIN" >/dev/stderr
}
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 after 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]}"
[[ $robtest -eq 1 ]] && echo "getzoneaction zone ${zname[$idx]}"
thisaction="n/a"
if [[ ${zignore[$idx]} -eq 1 ]]; then
problem="n/a"
thisperfect=1
else
[[ $robtest -eq 1 ]] && echo " owner of ${zname[$idx]} is [${zowner[$idx]}]" >&2
if [[ -z ${zowner[$idx]} ]]; then
pingok=1
#[[ $robtest -eq 1 ]] && echo " empty owner so no ping "
elif canping ${zowner[$idx]}; then
#[[ $robtest -eq 1 ]] && echo " canping returned ok"
pingok=1
else
#[[ $robtest -eq 1 ]] && echo " canping returned fail"
pingok=0
thisperfect=0
fi
fv=$(getforcevent ${zname[$idx]})
[[ $robtest -eq 1 ]] && echo " canping zone ${zname[$idx]} is: $pingok"
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"
[[ $robtest -eq 1 ]] && echo " zproblem is: ${zproblem[$idx]}"
[[ $robtest -eq 1 ]] && echo " zpri is: ${zpri[$idx]}"
[[ $robtest -eq 1 ]] && echo " zownerhome is: ${zownerhome[$idx]}"
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" || $thisaction == *set_mode: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" || $thisaction == *set_mode: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
[[ $robtest -eq 1 ]] && echo " zaction is: ${zaction[$idx]}"
[[ $robtest -eq 1 ]] && echo " zperfect is: ${zperfect[$idx]}"
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 [[ $conditions == *open* && $constant == ${zname[$zi]} ]]; then
# the constant zone never counts as an adjacent 'open' zone, because
# if we were to close it, it would just get opened again.
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
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -en "${GREEN}${BOLD}>> ${PLAIN}${GREEN}Determining actions... ${PLAIN}"
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* ]]; then
if [[ $constant == ${zname[$x]} ]]; then
if [[ $airconmyzoneid != ${zid[$x]} ]]; then
zaction[$x]="power_off"
[[ -z $powerchange ]] && powerchange="power_off"
fi
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 we are closing the constant zone and taking no other action,
# just turn the system off.
if [[ $nairconcommands -eq 1 ]]; then
if [[ ${airconjcmd[0]} == *lose* ]]; then
if [[ ${airconcmdzone[0]} == $constant ]]; then
globprob="constant_close"
globaction="power_off"
[[ -z $powerchange ]] && powerchange="power_off"
fi
fi
fi
if [[ -n $globaction ]]; then
gen_aircon_command -1 ${globaction}
fi
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -e "${GREEN}${BOLD}ok${PLAIN}"
}
# gen_aircon_command zone_idx "command1 command2 etc"
function gen_aircon_command() {
local idx allactions this toadd num doneset othername otherid otherid2 otheridx db donemode str jtoadd="" id2
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" '"info":{"state":"on"}'
elif [[ $this == *power_off* ]]; then
add_aircon_command $idx -1 "Power off system" "off" '"info":{"state":"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]}" "\"info\":{\"myZone\":${zid[$idx]}}"
elif [[ $this == *open:* ]]; then
# open vent in another zone
[[ $idx -eq -1 ]] && continue
othername=$(echo "$this" | sed -e 's/^.*open://;s/ .*//')
otheridx=$(getidxfromname $othername)
otherid=${zid[$otheridx]}
otherid2=$(printf "z%02d" "$otherid")
add_aircon_command $idx $otheridx "Open vent in ${zname[$otheridx]}" "set --zone ${otherid} --state on --temp ${zsettemp[$otheridx]}" "\"zones\":{\"${otherid2}\":{\"state\":\"open\",\"setTemp\",${zsettemp[$otheridx]}}"
elif [[ $this == *close:* ]]; then
# close vent in another zone
[[ $idx -eq -1 ]] && continue
othername=$(echo "$this" | sed -e 's/^.*close://;s/ .*//')
otheridx=$(getidxfromname $othername)
otherid=${zid[$otheridx]}
otherid2=$(printf "z%02d" "$otherid")
add_aircon_command $idx $otheridx "Close vent in ${zname[$otheridx]}" "set --zone ${otherid} --state off --temp ${zsettemp[$otheridx]}" "\"zones\":{\"${otherid2}\":{\"state\":\"close\",\"setTemp\",${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" "\"info\":{\"mode\":\"${str}\"}"
elif [[ $this == *set_temp* || $this == *open_vent* || $this == *close_vent* ]]; then
[[ $idx -eq -1 ]] && continue
if [[ $doneset -eq 0 ]]; then
id2=$(printf "z%02d" "${zid[$idx]}")
jtoadd="\"zones\":{\"${id2}\":{"
toadd=""
comm=""
if [[ $allactions == *open_vent* ]]; then
toadd="$toadd --state on"
jtoadd="${jtoadd}\"state\":\"open\","
comm="open vent"
elif [[ $allactions == *close_vent* ]]; then
toadd="$toadd --state off"
jtoadd="${jtoadd}\"state\":\"close\","
comm="close vent"
fi
if [[ $allactions == *set_temp* ]]; then
num=$(echo "$this" | sed -e 's/^.*set_temp://;s/ .*//')
toadd="$toadd --temp $num"
jtoadd="${jtoadd}\"setTemp\":\"$num\","
[[ -n $comm ]] && comm="${comm} and "
comm="${comm}set temperature to $num degrees"
else
toadd="$toadd --temp ${zsettemp[$idx]}"
jtoadd="${jtoadd}\"setTemp\":\"${zsettemp[$idx]}\","
fi
if [[ -n $toadd ]]; then
jtoadd="${jtoadd/%,/}}}" # remove trailing comma, add closing braces
add_aircon_command $idx -1 "In zone ${zname[$idx]}, $comm" "set --zone ${zid[$idx]}$toadd" "$jtoadd"
doneset=1
fi
fi
fi
done
}
# add_aircon_command zone_idx zone_idx2 "comment goes here" "textual version of command to run" "json command"
function add_aircon_command() {
local x idx otheridx comment db cmd jcmd
[[ $# -le 1 ]] && return 1
idx=$1
shift
otheridx=$1
shift
comment=$1
shift
cmd="$1"
shift
jcmd="{ \"ac1\":{"
jcmd="${jcmd}$1"
jcmd="${jcmd} } }"
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]="$cmd"
airconjcmd[$nairconcommands]="$jcmd"
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]}"
if [[ ${zproblem[$idx]} == "n/a" ]]; then
# global problem
if [[ -n $globprob ]]; then
airconproblem[$nairconcommands]="$(describe_globprob)"
else
airconproblem[$nairconcommands]="Unknown"
fi
else
# zone-specific problem
airconproblem[$nairconcommands]="${zname[$idx]} is ${zproblem[$idx]/_/ }"
fi
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 weekdaytofull() {
local full
shopt -s nocasematch
num=-1
case "$1" in
"mon") full="Monday";;
"tue") full="Tuesday";;
"wed") full="Wednesday";;
"thu") full="Thursday";;
"fri") full="Friday";;
"sat") full="Saturday";;
"sun") full="Sunday";;
esac
shopt -u nocasematch
echo "$full"
}
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
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -en "${GREEN}${BOLD}>> ${PLAIN}${GREEN}Loading config file... ${PLAIN}"
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"
if [[ $cronmode -eq 0 && $logmode -eq 0 ]]; then
if [[ $rv -eq 0 ]]; then
echo -e "${GREEN}${BOLD}ok${PLAIN}"
else
echo -e "${RED}${BOLD}fail${PLAIN}"
fi
fi
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
local db=0
if [[ $RULEDB -eq 1 ]]; then
db=1
fi
if [[ $1 == "-c" ]]; then
keepgoing=1
shift 1
fi
line="$*"
timestr_human=""
timestr=${line%% *} # only process first word
timestr=${timestr:1} # strip leading @
[[ $db -eq 1 ]] && info "${BOLD}Processing line: '$line'"
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)
[[ -n $timestr_human ]] && this_human="o " || this_human=O
this_human=$(printf "${this_human}n _H_%ss_EH_" $(weekdaytofull ${ttok[0]}))
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[0]} == ${ttok[1]} ]]; 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 # end if ttokcount
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
[[ $db -eq 1 ]] && info " time period does NOT span midnight"
if [[ $(echo "$nowh >= $starth" | bc) == "1" && $(echo "$nowh <= $endh" | bc) == "1" ]]; then
ok=1
fi
elif [[ $(echo "$starth > $endh" | bc) == "1" ]]; then
[[ $db -eq 1 ]] && info " time period spans midnight"
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 # end if condition matches
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="<font color=#000000></strong></s>" # black
inactc="<font color=#ff0000>" # red
linec="<font color=#000000></strong></s>" # black
devc="<font color=#00aa00>" # green+bold
devbc="<strong><font color=#00aa00>" # green
statec="<strong><font color=#bbbb00>" # yellow+bold
roomc="<font color=#ffa500>" # orange
timec="<font color=#ff97c6>" # pink
timebc="<strong><font color=#ff97c6>" # pink+bold
minc="<strong><font color=#0000dd>" # blue+bold
maxc="<strong><font color=#dd0000>" # red+bold
coolc="<strong><font color=#00dddd>" # cyan+bold
heatc="<strong><font color=#dd0000>" # red+bold
nl="<br></s></strong><font color=#000000>\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="<font color=#000000></strong></s>" # black
inactc="<s><font color=#aaaaaa>" # lightgrey
linec=""
devc=""
devbc=""
statec=""
roomc=""
timec=""
timebc=""
minc=""
maxc=""
nl="</s><br>\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
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -en "${GREEN}${BOLD}>> ${PLAIN}${GREEN}Parsing config file... ${PLAIN}"
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"
[[ $ok -eq 1 ]] && 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
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -e "${GREEN}${BOLD}ok${PLAIN}"
if [[ $show -eq 2 ]]; then
echo -e "${config_human}"
elif [[ $show -eq 1 ]]; then
info "Configuration file is ${BOLD}OK"
fi
rv=0
else
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -e "${RED}${BOLD}fail${PLAIN}"
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 n hname ip db host success=0 mac
local os arprv arpres
os=$(uname -s)
db=0
if [[ $db -eq 1 ]]; then
info "canping() for:"
for host in $*; do
info " - '${host}'"
done
fi
# get ip and clear arp
n=0
for host in $*; do
hname[$n]="$host"
if [[ $os == "Darwin" ]]; then
ip[$n]=$(dscacheutil -q host -a name $host | grep ^ip_address: | awk '{ print $NF}')
else
ip[$n]=$(getent hosts $host | grep -v : | awk '{ print $1 }')
fi
if [[ -n ${ip[$n]} ]]; then
arp -d ${ip[$n]} >/dev/null 2>&1
fi
n=$((n + 1))
done
# send new arp req
for n in ${!ip[@]}; do
if [[ -n ${ip[$n]} ]]; then
if [[ -e $ARPING ]]; then
${ARPING} -c10 -W0.2 -w4 -C1 -q ${ip[$n]} 2>/dev/null &
else
ping -c 1 -w 1 -q ${ip[$n]} 2>/dev/null &
fi
fi
done
# wait for arp replies or timeout
wait
sleep 0.4
# now check entries
for n in ${!ip[@]}; do
[[ $db -eq 1 ]] && info " recheck ${hname[$n]} (${ip[$n]})"
if [[ -n ${ip[$n]} ]]; then
arpres=$(arp -n ${ip[$n]} 2>/dev/null)
arprv=$?
mac=$(echo "$arpres" | egrep -v "Host|xpired" | awk '{print $2}')
[[ $mac == *:* ]] && success=1
fi
[[ $db -eq 1 ]] && info " ${hname[$n]} (${ip[$n]}) mac is mac=$mac"
[[ $db -eq 1 ]] && info "canping() ${hname[$n]} (${ip[$n]}) is $success"
if [[ $success -eq 1 ]]; 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() {
local jsoninfo url state
profile "query aircon"
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -en "${GREEN}${BOLD}>> ${PLAIN}${GREEN}Querying aircon... ${PLAIN}"
url="${AIRCON_URL}/getSystemData"
jsoninfo=$(curl -s "$url" 2>/dev/null)
if [[ $? -ne 0 ]]; then
error "get_aircon_info() failed to reach ${AIRCON_URL}/getSystemData endpoint"
exit 1;
fi
echo "$jsoninfo" | jq . &>/dev/null
if [[ $? -ne 0 ]]; then
error "${AIRCON_URL}/getSystemData returned bad json: $jsoninfo"
exit 1;
fi
zones=$(echo "$jsoninfo" | $JQ -r '.aircons.ac1.zones[] | [ .name, .state, .setTemp, .measuredTemp, .number ] | @csv' | tr -d '" ')
state=$(echo "$jsoninfo" | $JQ -r '.aircons.ac1.info.state' | tr -d '" ')
if [[ $state == "off" ]]; then
airconmode="off"
else
airconmode=$(echo "$jsoninfo" | $JQ -r '.aircons.ac1.info.mode' | tr -d '" ')
fi
airconmyzoneid=$(echo "$jsoninfo" | $JQ -r '.aircons.ac1.info.myZone ' | tr -d '" ')
nzones=0
for line in $zones; do
IFS=',' read -ra tok <<< "$line"
addzone "${tok[0]}" "${tok[1]}" "${tok[2]}" "${tok[3]}" "${tok[4]}"
done
if [[ $nzones -eq 0 ]]; then
error "get_aircon_info() found no zones"
exit 1;
fi
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="<off>"
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
}
# generate textual description of global problem
function describe_globprob() {
[[ -z $globprob ]] && return 1;
if [[ $globprob == too_* ]]; then
echo "$limit or more zones are ${globprob/_/ }"
elif [[ $globprob == "not_needed" ]]; then
echo "The system is on but all zones at optimal temperature"
elif [[ $globprob == "constant_close" ]]; then
echo "All other zones are already closed"
else
echo "_unknown_globprob_:$globprob"
fi
return 0
}
function show_analysis() { # returns 1 if all good and nothing to show
local count x ostr
local amode="normal"
if [[ $1 == "-t" ]]; then
mode="telegram"
shift
fi
if [[ $mode != "telegram" ]]; then
echo -e "${UNDERLINE}Analysis:${PLAIN}"
fi
count=0
for x in ${!zproblem[@]}; do
znowners=$(wc -w <<< "${zowner[$x]}" | bc)
zowners=$(sed -r 's/^ +//;s/ +$//;s/ +/, /g' <<< "${zowner[$x]}")
if [[ ${zproblem[$x]} != "n/a" ]]; then
if [[ ${zproblem[$x]} == "owner_not_home" ]]; then
if [[ $znowners -gt 1 ]]; then
ostr="owners are"
else
ostr="owner is"
fi
echo "- The zone '${zname[$x]}' is open but its $ostr not home (${zowners})"
elif [[ ${zproblem[$x]} =~ force_ ]]; then
what=${zproblem[$x]##*_}
[[ $what == "close" ]] && what="${what}d"
echo "- The zone '${zname[$x]}' should be $what at this time"
else
if [[ ${zownerhome[$x]} -ne 1 ]]; then
if [[ $znowners -gt 1 ]]; then
append1=", but none of its owners ($zowners) are 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 "- The zone '${zname[$x]}' is ${zproblem[$x]/_/ }${append1}${append2}"
fi
count=$((count + 1))
fi
done
if [[ -n $globprob ]]; then
echo "- $(describe_globprob)"
fi
if [[ $count -eq 0 && -z $globprob ]]; then
if [[ $mode != "telegram" ]]; then
echo -e "${GREEN}All is good!${PLAIN}"
fi
fi
[[ $count -eq 0 && -z $globprob ]] && return 1
return 0;
}
function killfile_active() { # returns true if killfile will stop us
local val now
if [[ ! -f ${KILLFILE} ]]; then
return 1
fi
val=$(head -1 "${KILLFILE}")
[[ -z $val ]] && return 0
now=$(date +%s)
if [[ $val -le $now ]]; then
# killfile expired
rm -f "${KILLFILE}"
if [[ -n $expirenotifyfile ]]; then
echo "$expirenotifytext" > "$expirenotifyfile"
fi
return 1
fi
return 0
}
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 killfile_active; 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 combine_commands() {
local x id2 basefile tfile combinejq jcmd jurl
combinejq=".[0]"
if [[ ${#airconjcmd} -eq 0 ]]; then
return 1
fi
basefile=$(mktemp /tmp/$$.json.base.XXXXXX)
[[ -z $basefile ]] && { error "couldnt create base json file" >&2; exit 1; }
echo -e "{\n\"aircons\": {\n\"ac1\": {\n\"info\": {\n},\n\"zones\": {\n" >${basefile}
for x in ${zid[@]}; do
id2=$(printf "%02d" $x)
echo " \"z${id2}\": { }," >>${basefile}
done
sed -i '$s/,$//' ${basefile}
echo -e "}\n}\n}\n}\n" >> ${basefile}
for x in ${!airconjcmd[@]}; do
tfile[$x]=$(mktemp /tmp/$$.json.$x.XXXXXX)
[[ -z ${tfile[$x]} ]] && { error "couldnt create temp file" >&2; exit 1; }
echo "${airconjcmd[$x]}" >${tfile[$x]}
combinejq="${combinejq} * .[$((x + 1))]"
done
jcmd=$($JQ -s "$combinejq" $basefile ${tfile[@]} )
[[ $? -ne 0 ]] && { error "couldnt merge json command files " >&2; exit 1; }
[[ -z $jcmd ]] && { error "got empty merged json command" >&2; exit 1; }
jurl="${AIRCON_URL}/setAircon?json=$jcmd"
for x in ${tfile[@]};
do rm -f "${x}"
done
rm -f "$basefile"
echo "$jurl" | tr -d '\n '
return 0
}
# note: this function is no longer used
function reword_problem() { #1="problem text"
local orig="$1" new=""
if [[ $orig == *" is too "* ]]; then
new=$(sed -r 's/^([A-Za-z]+) is too ([A-Za-z]+)/the zone "\1" was too \2/' <<<"$orig")
elif [[ $orig == *" is owner not_home "* ]]; then
new=$(sed -r 's/^([A-Za-z]+) is .*/the owner of the zone "\1" was not at home/' <<<"$orig")
elif [[ $orig == *" is force close"* ]]; then
new=$(sed -r 's/^([A-Za-z]+) is .*/the zone "\1" should be closed at this time/' <<<"$orig")
elif [[ $orig == *" is n/a"* ]]; then
## wrong! n/a means there is a globprob
new=$(sed -r 's/^([A-Za-z]+) is .*/the zone "\1" should be the "myzone" at this time/' <<<"$orig")
else
new="$orig"
fi
new=$(sed 's/&/%26/g' <<<"$new")
echo "$new"
}
function telegram_send() { #1=msg
local res msg="$1"
#action "sending to telegram: curl -s --data \"text=$msg\" --data \"chat_id=$TELEGRAM_CHAT\" --data \"parse_mode=markdown\" 'https://api.telegram.org/bot'$TELEGRAM_TOKEN'/sendMessage' "
res=$(curl -s --data "text=$msg" --data "chat_id=$TELEGRAM_CHAT" --data "parse_mode=markdown" 'https://api.telegram.org/bot'$TELEGRAM_TOKEN'/sendMessage' 2>&1)
if [[ $cronmode -eq 1 ]]; then
echo "telegram_send: text=[$msg]"
echo "telegram_send: curl=curl -s --data \"text=$msg\" --data \"chat_id=$TELEGRAM_CHAT\" --data \"parse_mode=markdown\" \'https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage\'"
echo "telegram_send: res=[$res]"
fi
#action "telegram result: $res"
}
function run_commands() {
local x tfile combinejq jcmd jurl res jqres
local thisprob thisfix prevprob
local botwords="" txt fixnum
for x in ${!airconcmd[@]}; do
#echo RUNNING myair $AIRCON_IP ${airconcmd[$x]}
[[ -n ${airconcomment[$x]} ]] && action "${airconcomment[$x]}"
influx_insert "INSERT aircon action=\"${airconcomment[$x]}\",comment=\"${airconproblem[$x]}\""
done
jurl=$(combine_commands)
if [[ $? -eq 0 ]]; then
res=$(curl -s -g "$jurl" 3>/dev/null)
jqres=$(echo "$res" | $JQ -r '.ack' 2>/dev/null)
if [[ $jqres != "true" ]]; then
error "Myair API call failed:"
echo -e "$RED curl -s -g $jurl$PLAIN"
echo -e "$RED Result:"
echo "$jqres" | sed 's/_^/ /'
return 1
fi
fi
}
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
}
function show_telegram_output() {
local botwords="" atext fixnum=1 thisprob thisfix
# get analysis text in telegram mode
atext=$(show_analysis -t)
[[ $? -ne 0 ]] && return 1;
# replace newlines
atext="$(sed ':begin;$!N;s/\n/%0a/;tbegin' <<<"$atext")"
# replace ampersands
atext=$(sed 's/&/%26/g' <<<"${atext}")
if [[ $? -eq 0 ]]; then
botwords="I noticed that:%0a"
botwords="${botwords}${atext}%0a%0a"
botwords="${botwords}I'm going to do this to fix it:%0a"
fixnum=1
for x in ${!airconcmd[@]}; do
thisprob=${airconproblem[$x]}
thisfix=$(sed 's/&/%26/g' <<<"${airconcomment[$x]}")
if [[ $cronmode -eq 1 ]]; then
echo "telegram_debug: thisprob=[$thisprob]"
echo "telegram_debug: airconcomment[$x]=[${airconcomment[$x]}]"
echo "telegram_debug: thisfix=[$thisfix]"
fi
botwords="$botwords$(printf ' %d. %s%%0a' $fixnum "$thisfix")"
fixnum=$((fixnum + 1))
done
fi
[[ -n $botwords && $fixnum -gt 1 ]] && telegram_send "$botwords"
}
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
USETELEGRAM=""
TELEGRAM_FILE=""
TELEGRAM_CHAT=""
TELEGRAM_TOKEN=""
# 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 $*"
expirenotifyfile=""
expirenotifytext=""
FAKESTRING=""
optstring="aA:bcD:e:E:f:F:g:hHi:I:k:l:Lo:pmRsSt:T:wWy"
while getopts "$optstring" i $ALLARGS; do
case "$i" in
a)
ARPING=${OPTARG}
;;
A)
influxdb="$OPTARG"
;;
b)
RULEDB=1
;;
c)
enable_cronmode
;;
D)
influxhost="${OPTARG}"
;;
e)
expirenotifyfile="$OPTARG"
;;
E)
expirenotifytext="$OPTARG"
;;
f)
CONFIGFILE=${OPTARG}
;;
F)
FAKESTRING="${FAKESTRING} ${OPTARG}"
;;
g)
USETELEGRAM=1
TELEGRAM_FILE=${OPTARG}
;;
h)
usage;
exit 1;
;;
H)
RULEFORMAT=html
;;
i)
AIRCON_IP=${OPTARG}
;;
I)
logmode=2
influxdb="$OPTARG"
;;
k)
KILLFILE=${OPTARG}
;;
l)
limit=${OPTARG}
;;
L)
logmode=1
;;
m)
makeconfig=1
;;
o)
csvfile="$OPTARG"
;;
p)
profiler=1
info "Profiler mode enabled"
;;
R)
robtest=1
;;
s)
sanitycheck=1
;;
S)
sanitycheck=2
;;
t)
tolerance_l=${OPTARG}
;;
T)
tolerance_h=${OPTARG}
;;
w)
showwho=1
;;
W)
showwho=2
;;
y)
DOIT=1
;;
*)
usage;
exit 1;
;;
esac
done
shift $((OPTIND - 1))
AIRCON_URL="http://${AIRCON_IP}:2025"
if [[ -n $expirenotifyfile && -z $expirenotifytext ]]; then
error "Cannot use -e option without -E."
exit 1
fi
if [[ ! -e $ARPING ]]; then
warn "arping binary '$ARPING' not found, will use ping instead"
fi
if [[ -n $influxdb ]]; then
influx_init "$influxdb" || exit 1
fi
if [[ $USETELEGRAM -eq 1 ]]; then
if [[ -z $TELEGRAM_FILE || ! -f $TELEGRAM_FILE ]]; then
error "Telegram auth file '$TELEGRAM_FILE' doesn't exist."
exit 1
fi
TELEGRAM_TOKEN=$(awk NF $TELEGRAM_FILE | head -1)
TELEGRAM_CHAT=$(awk NF $TELEGRAM_FILE | tail -1)
if [[ -z $TELEGRAM_TOKEN ]]; then
error "Telegram token is empty. Check that auth file '$TELEGRAM_FILE' first line is token, second line is chat id."
exit 1
fi
if [[ -z $TELEGRAM_CHAT ]]; then
error "Telegram chat id is empty. Check that auth file '$TELEGRAM_FILE' first line is token, second line is chat id."
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
JQ=$(which jq 2>/dev/null)
if [[ $? -ne 0 ]]; then
error "Can't find jq executable in path."
exit 1
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
# fake input for testing
for x in ${FAKESTRING}; do
fake_name="${x%%:*}"
fake_wanttemp="${x##*:}"
fake_idx=-1
for zi in ${!zname[@]}; do
if [[ ${zname[zi]^^} == ${fake_name^^} ]]; then
fake_idx=$zi
break
fi
done
if [[ ${fake_idx} -eq -1 ]]; then
error "Unknown zone '${fake_name}' in -f argument '${x}'"
exit 1;
fi
ztemp[$fake_idx]="$fake_wanttemp"
info "Faking zone '${fake_name}' temp to ${fake_wanttemp}"
done
# 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
maxlen=$((maxlen + 1))
# 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
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -en "${GREEN}${BOLD}>> ${PLAIN}${GREEN}Querying zone owners... ${PLAIN}"
# 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' ' ')
n=0
for x in $alldevs; do
canping ${x} 2>&1
pingres[$n]=$?
n=$((n + 1))
done
[[ $cronmode -eq 0 && $logmode -eq 0 ]] && echo -e "${GREEN}${BOLD}ok${PLAIN}"
echo
printf "$TFORMAT" "Devices" "Availability" "Zones owned"
n=0
for x in $alldevs; do
if [[ ${pingres[$n]} -eq 0 ]]; 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}"
n=$((n + 1))
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 ! killfile_active; then
run_commands
[[ $cronmode -ne 1 ]] && echo
if [[ $USETELEGRAM -eq 1 ]]; then
show_telegram_output
fi
fi
fi