better config file parsing

-s option to verify config file
-S option to show rules in human readable format
fix for dns resolution on osx where getent isn't available
change tabs to spaces
make -w switch work without pymyair
This commit is contained in:
Rob Pearce 2021-12-04 20:24:25 +11:00
parent 6848a09bb7
commit 8ccbdc902c
2 changed files with 1410 additions and 1238 deletions

0
README.md Normal file → Executable file
View File

310
aircon.sh
View File

@ -17,13 +17,18 @@ VALID_ZONE_STATES=" open close "
BOLD="\033[1m"
PLAIN="\033[0m"
STRIKE="\033[9m"
UNDERLINE="\033[4m"
GREY="\033[1;30m"
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"
@ -60,6 +65,8 @@ function usage() {
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 Show configured rules in human-readable format."
echo " -w List which zone owners' devices are available then exit."
echo " -t num Specify degrees below min temperature before taking action."
echo " -T num Specify degrees above max temperature before taking action."
@ -791,7 +798,7 @@ function getstrtype() {
echo "$strtype"
}
function numtoweekday() {
function weekdaytonum() {
local num
shopt -s nocasematch
num=-1
@ -818,13 +825,66 @@ 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
line=${line%%#*} # remove comments
if [[ ! -z ${line// } ]]; then
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 ${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 ${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
if [[ $1 == "-c" ]]; then
keepgoing=1
shift 1
fi
line="$*"
timestr_human=""
timestr=${line%% *}
timestr=${timestr:1}
IFS=';' read -ra allconds <<< "$timestr"
@ -837,7 +897,7 @@ function load_config() {
error "Invalid condition: '${cond}'"
return 1
fi
starth=$(numtoweekday ${ttok[0]})
starth=$(weekdaytonum ${ttok[0]})
if [[ $starth -eq -1 ]]; then
error "'${ttok[0]}' is not a valid type here (type is ${stype[0]})"
@ -860,15 +920,25 @@ function load_config() {
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=$(numtoweekday ${ttok[0]})
endh=$(numtoweekday ${ttok[1]})
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
@ -893,72 +963,163 @@ function load_config() {
[[ $db -eq 1 && $ok -eq 0 ]] && info " $nowh isn't in range $cond"
[[ $db -eq 1 && $ok -eq 1 ]] && info " MATCH: $nowh within range $cond"
fi
if [[ $ok -eq 0 ]]; then
timestr_human="${timestr_human}${this_human}"
if [[ $ok -eq 0 && $keepgoing -eq 0 ]]; then
break
fi
done
if [[ $ok -eq 1 ]]; then
# strip condition off the front
line=${line#* }
return $((1 - $ok))
}
function active_cols() {
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"
}
function inactive_cols() {
inactc="$DARKGREY"
linec="${inactc}"
devc="$inactc"
devbc="$inactc"
statec="$inactc"
roomc="$inactc"
timec="$inactc"
timebc="$inactc"
minc="$inactc"
maxc="$inactc"
}
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
if [[ -n $1 ]]; then
show=$1
fi
else
db=0
rv=0
config_human=""
errstr=""
linenum=1
while read line; do
ok=1
fi
if [[ $ok -eq 1 ]]; then
IFS=' ' read -ra tok <<< "$line"
if [[ ${tok[0]} == "temp" ]]; then
addtemprange ${tok[1]} ${tok[2]}
elif [[ ${tok[0]} == "constant" ]]; then
if [[ -z ${tok[1]} ]]; then
error "Missing zone name in constant command."
exit 1
elif [[ -z $constant ]]; then
constant=${tok[1]}
else
error "Constant zone defined more than once."
exit 1
fi
elif [[ ${tok[0]} == "adj" ]]; then
addadj ${tok[1]} ${tok[@]:2}
elif [[ ${tok[0]} == "nomyzone" || ${tok[0]} == "nomy" ]]; then
addnomyzone ${tok[1]}
elif [[ ${tok[0]} == "noop" || ${tok[0]} == "ignore" ]]; then
addnoop ${tok[1]}
elif [[ ${tok[0]} == "force" ]]; then
if [[ $VALID_ZONE_STATES == *\ ${tok[2]}\ * ]]; then
addforcevent ${tok[1]} ${tok[2]}
else
error "Invalid zone state '${tok[2]}'. Valid options are: $VALID_ZONE_STATES"
exit 1
fi
elif [[ ${tok[0]} == "owner" ]]; then
addowner ${tok[1]} ${tok[@]:2}
elif [[ ${tok[0]} == "person" ]]; then
addperson ${tok[1]} ${tok[@]:2}
elif [[ ${tok[0]} == "modelock" ]]; then
if [[ $VALID_MODES == *\ ${tok[1]}\ * ]]; then
modelock="${tok[1]}"
else
error "Invalid modelock '${tok[1]}'. Valid options are: $VALID_MODES"
exit 1
fi
elif [[ ${tok[0]} == "test" ]]; then
info "Got test option: '${tok[@]}'"
rv=1
active_cols
line=${line%%#*} # remove comments
line_human=""
if [[ ! -z ${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]#*-}
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"
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\n"
fileok=0
fi
elif [[ ${tok[0]} == "owner" ]]; then
local devices verb
devices="${devc}${tok[@]:2}${linec}"
devices="${devbc}[${linec}${devices}${devbc}]${linec}"
if [[ ${#tok[@]} -eq 3 ]]; then
verb="isn't"
else
verb="aren't"
fi
line_human="${line_human}${statec}Close${linec} vent in ${roomc}${tok[1]}${linec} if $devices $verb online"
elif [[ ${tok[0]} == "modelock" ]]; then
if [[ $VALID_MODES == *\ ${tok[1]}\ * ]]; then
local col
modelock="${tok[1]}"
[[ $modelock == "cool" ]] && col="$BOLD$CYAN" || col="$RED"
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\n"
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}\n"
fileok=0
fi
fi
if [[ -n $line_human ]]; then
if [[ $ok -eq 0 ]]; then
line_human="${PLAIN}${inactc}<INACTIVE> ${linec}${line_human}"
fi
config_human="${config_human}- ${linec}${line_human}${PLAIN}\n"
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${PLAIN}" | 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
@ -1343,6 +1504,7 @@ showwho=0
limit=$DEFAULTLIMIT
profiler=0
csvfile="$DEFAULT_CSVFILE"
sanitycheck=0
# check for config file option first
@ -1361,7 +1523,7 @@ fi
ALLARGS="$ALLARGS $*"
optstring="A:cf:hi:I:k:l:Lo:pymwt:T:x:"
optstring="A:cf:hi:I:k:l:Lo:pmsSt:T:wx:y"
while getopts "$optstring" i $ALLARGS; do
case "$i" in
h)
@ -1409,6 +1571,12 @@ while getopts "$optstring" i $ALLARGS; do
m)
makeconfig=1
;;
s)
sanitycheck=1
;;
S)
sanitycheck=2
;;
y)
DOIT=1
;;
@ -1426,20 +1594,6 @@ while getopts "$optstring" i $ALLARGS; do
done
shift $((OPTIND - 1))
if [[ -z $MYAIR ]]; then
MYAIR=$(which myair)
if [[ $? -ne 0 ]]; then
error "Can't find pymyair executable 'myair' in path. Install it from here: https://github.com/smallsam/pymyair"
exit 1
fi
else
if [[ ! -x "$MYAIR" ]]; then
error "Specified pymyair executable '$MYAIR' not found."
exit 1
fi
fi
if [[ $makeconfig -eq 1 ]]; then
gen_config
rv=$?
@ -1452,7 +1606,10 @@ if [[ $makeconfig -eq 1 ]]; then
fi
if [[ -e $CONFIGFILE ]]; then
if ! load_config; then
if [[ $sanitycheck -ge 1 ]]; then
parse_config $sanitycheck
exit $?
elif ! load_config; then
error "Config load failed"
exit 1
fi
@ -1461,6 +1618,21 @@ else
exit 1
fi
if [[ $showwho -ne 1 ]]; 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
# ping all hosts in background to populate arp table
for x in ${devices}; do
ping -c1 -w1 -n -q $ip >/dev/null 2>&1 &