#!/bin/sh

# Query W&T Web-IO devices and store temperature/humidity values in an 
# RRDtool database. Call the script without parameters for a summary of
# available options. Additional parameters can be set by editing the 
# script itself, see below.
# Hardware: http://www.wut.de
# Software: http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/
#
# Limitations of this script:
# - "One file per device": Each database file may record some or all of the
#   sensors of one particular device, but additional devices always require
#   additional database files.
# - RRDtool itself will let you combine data from more than one database 
#   into one diagram, but this script won't. If you should need that feature,
#   take a look at our plotting routines in verbose mode (-v), then copy
#   their rrdtool invocations into a separate script and edit them to do 
#   what you really want.
# - This script is most convenient for querying one single Web-IO device.
#   It is also suitable for several devices of the same kind (i.e. with 
#   the same number and type of sensors): You can run several instances of 
#   it, each with different command line parameters for database and device
#   IP address.
#   A "server farm" with a wide variety of devices, however, is best handled
#   by making several copies of the script, each edited to match one device.

# =======================================================================
# PLEASE EDIT THE VARIABLES IN THIS SECTION FOR YOUR PURPOSES.

# Database file name, should end with .rrd
# The file itself will be created, however the directory must already exist.
DBASE=~/thermo.rrd

# IP address of the Web-IO device
TGT_DEVICE=172.16.232.193

# List of the sensors to query, numbers starting from zero. This would be
# "0", for example, for a 1x thermometer, "01" for a thermo-hygrometer,
# and "0123" to read only the first four sensors on an 8x thermometer.
# PLEASE edit this value appropriately. The more sensors are listed here,
# no matter if all of them are present on your device, the larger your 
# database will be.
SENSORS="0"

# Sensor names, as they are stored in the database.
# This set is correct for all thermometers, from 1x to 8x:
NAMES="T0 T1 T2 T3 T4 T5 T6 T7"
# Use this set instead for the thermo-hygrometer:
#NAMES="T0 H0"

# Parameters for the round-robin archives. Recommended values are given in
# parentheses. The first two archive levels are calculated as the average
# of all actual sensor readings in the corresponding interval.
# We keep a few days (7 - 30) at the highest resolution (1 - 10 minutes).
FINEST_RES=5
FINEST_DAYS=7
# We keep a few months (3 - 12) at a lower resolution (10 - 60 minutes).
MEDIUM_RES=60
MEDIUM_MONTHS=3
# After that, we keep only the daily min/max/avg values, but for several 
# years. (Please keep in mind here, that RRDtool database files never grow,
# but are immediately allocated at the maximum size that will ever be
# required. So setting a storage time of 100 years, for example, would not
# be the brightest idea, unless you really mean it.)
DAILY_YEARS=10

# Expected range of values for each sensor. RRDtool will refuse to record
# a value if it exceeds this range. We choose -60 through 120 by default,
# because that should safely enclose all values that a Web Thermometer can
# ever report. 
CLIPPING="-60:120 -60:120 -60:120 -60:120 -60:120 -60:120 -60:120 -60:120"
# If you want to be really sure, you can disable range checking altogether:
# The "U" stands for "unlimited".
#CLIPPING="U:U U:U U:U U:U U:U U:U U:U U:U"

# (end of application-specific variables)
# =======================================================================
# MORE VARIABLES, HOWEVER THESE APPLY TO DIAGRAM OUTPUT ONLY.

# The filename for the output diagram, should end with .png.
# RRDtool can create various other file formats (including SVG, EPS, PDF), 
# but PNG is what this script does.
DIAGRAM=./diagram.png

# You will not usually want to plot the whole contents of your database,
# but just go back a few days in time.
STDPLOT_DAYS=8

# The default range for the min/max diagram. Since we've got only one
# min/max value per day, it doesn't hurt to make this range much longer.
MNMXPLOT_DAYS=100

# The min/max diagrams usually look better with areas rather than pairs
# of lines. (Set this variable to either "area" or "lines".)
MNMXPLOT_STYLE="area"

# Diagram colors for the sensors, as rrggbb hex values.
# Default: gray, black, red, blue, green, orange, cyan, dark green
COLORS="909090 000000 ff0000 0000ff 00ff00 ffaa00 00ccff 00aa00"

# Descriptive names for the sensors.
LABELS=( "Sensor 0" "Sensor 1" "Sensor 2" "Sensor 3" \
         "Sensor 4" "Sensor 5" "Sensor 6" "Sensor 7" )

# Note that you can override the SENSORS variable (see previous section)
# from the command line when plotting diagrams. This will allow you, for
# example, to create individual diagrams for each sensor, rather than one
# diagram for all.

# (end of variables for diagram creation)
# =======================================================================


# RRDTOOL MAY HAVE BEEN RENAMED.
# For example, this is its standard name on SuSE Linux 7:
rrdtool=i386-suse-linux-rrdtool
#rrdtool=rrdtool


# A rather nasty portability issue for this script (or this whole
# procedure, if you wish) is the degree sign 0xB0 in the reply string
# from the Web-Thermometer. It is easy to write a script that works nicely
# on systems with ISO-8859-1 as their default charset, and fails
# miserably with UTF-8. (This is because 0xB0 by itself is an illegal 
# character in UTF-8, and causes undefined behaviour. I had to find that
# on an UTF system, I could insert 0xB0 into a string, but not remove it
# again.)
#
# The iconv command is what comes in to save the day. We use it to convert 
# characters from the ISO-8859-1 charset to the system default charset, 
# whatever that may be. On systems lacking that command, you may set the
# following variable to either "iso" or "utf", depending on your actual
# environment. Otherwise, leave it as "auto".
CSMODE="auto"

if [ $CSMODE = "utf" ]; then
    DEGSIGN="°"
elif [ $CSMODE = "iso" ]; then
    DEGSIGN=""
else    
    DEGSIGN=$(echo "" | iconv -f iso-8859-1)
fi



# Look for optional parameters.
if [ "$1" = "-v" ]; then
    verbose="true"
    shift
fi
if [ -n "$2" ]; then
    DBASE=$2
fi
if [ -n "$3" ]; then
    TGT_DEVICE=$3
fi


# Polling interval, in seconds.
# This value is not meant to be changed, because Web-IO devices only acquire
# one sample value per minute, anyway.
# If you do change it, please note that FINEST_RES and MEDIUM_RES in the
# parameters above will change their meaning: They are just multiples of
# the polling interval, and will therefore no longer be minutes. All other
# parameters, however, will keep their meaning.
INTERVAL=60


# Create the database
if [ "$1" = "-c" ]
then
    if [ -e $DBASE ]; then
        echo "Error: $DBASE already exists"
        exit 1
    fi
    # Create the database with one data source for each sensor on the list.
    sources=""
    let heartbeat=$INTERVAL*2
    i=0
    set -- $CLIPPING
    for name in $NAMES; do 
        if [[ $SENSORS == *$i* ]]; then
            sources="$sources DS:$name:GAUGE:$heartbeat:$1"
        fi
        let i=$i+1
        shift
    done
    # Calculate the round-robin archive settings from the user-defined
    # parameters.
    let steps="($FINEST_DAYS*24*3600)/($FINEST_RES*$INTERVAL)"
    archives="RRA:AVERAGE:0.5:$FINEST_RES:$steps"
    let steps="($MEDIUM_MONTHS*30*24*3600)/($MEDIUM_RES*$INTERVAL)"
    archives="$archives RRA:AVERAGE:0.5:$MEDIUM_RES:$steps"
    let day="(24*3600)/$INTERVAL"
    let steps="($DAILY_YEARS*365*24*3600)/($day*$INTERVAL)"
    archives="$archives RRA:MIN:0.5:$day:$steps"
    archives="$archives RRA:MAX:0.5:$day:$steps"
    archives="$archives RRA:AVERAGE:0.5:$day:$steps"

    if [ -n "$verbose" ]; then
    	echo "$rrdtool create $DBASE --step $INTERVAL \\"
    	for i in $sources; do
    	    echo "  $i \\"
    	done
    	set -- $archives
    	while [ -n "$2" ]; do
    	    echo "  $1 \\"
    	    shift
    	done
    	echo "  $1"
    fi    
    $rrdtool create $DBASE --step $INTERVAL $sources $archives 
    exit 0
fi




# Show recently recorded contents of the database (last hour).
if [ "$1" = "-i" ]; then
    if [ -n "$verbose" ]; then
        echo "$rrdtool fetch $DBASE AVERAGE -s -3600"
    fi
    $rrdtool fetch $DBASE AVERAGE -s -3600
    exit 0
fi



# Create standard diagram (short range, fine resolution)
if [[ $1 == -d* ]]; then
    # optional number of days is stuck directly to the "-d"
    days=${1#-d}
    if [ -n "$days" ]; then
        STDPLOT_DAYS=$days
    fi
    # $3 has a different meaning here
    if [ -n "$3" ]; then
        DIAGRAM=$3
    fi
    # $4 selects a sensor subset for plotting
    if [ -n "$4" ]; then
        SENSORS=$4
    fi
    # Create (trivial) variable definitions and plot commands for all sensors.
    i=0
    defs=""
    plots=""
    set -- $COLORS
    for name in $NAMES; do 
        if [[ $SENSORS == *$i* ]]; then
            defs="$defs DEF:a$i=$DBASE:$name:AVERAGE"
            plots="$plots\"LINE1:a$i#$1:${LABELS[$i]}\" "
        fi
        shift
        let i=$i+1
    done
    let seconds=24*3600*$STDPLOT_DAYS
    # Remember how carefully we just crafted the contents of the $plots
    # variable, so that parameters containing spaces are enclosed in 
    # quotes? For the verbose output we are now going to split the command
    # parameters into words, and sadly, the quotes don't do here what we'd
    # expect them to. We have to do some fiddling to get the output right.
    if [ -n "$verbose" ]; then
        echo "$rrdtool graph $DIAGRAM -s -$seconds \\"
    	for i in $defs; do
    	    echo "  $i \\"
    	done
    	# Temporarily replace all spaces by underscore, except for those
    	# following a quote sign.
    	temp=${plots// /_}
    	set -- ${temp//\"_/\" }
    	while [ -n "$2" ]; do
    	    echo "  ${1//_/ } \\"
    	    shift
    	done
    	echo "  ${1//_/ }"
    fi
    # The following line may appear to do the same as executing the command
    # between "echo" and "| /bin/sh" directly. Well, it doesn't. Unless we
    # invoke our command in this complicated fashion, the quotes around the
    # parameters with spaces aren't properly recognized.
    echo $rrdtool graph $DIAGRAM -s -$seconds $defs $plots | /bin/sh
    exit 0
fi


# Create min/max diagram (long range, one sample per day)
# Only minor differences to the code for standard diagrams
if [[ $1 == -D* ]]; then
    days=${1#-D}
    if [ -n "$days" ]; then
        MNMXPLOT_DAYS=$days
    fi
    if [ -n "$3" ]; then
        DIAGRAM=$3
    fi
    if [ -n "$4" ]; then
        SENSORS=$4
    fi
    # Create variable definitions and plot commands, two for each sensor.
    # Each sensor gets just one color, and one field in the legend. The 
    # min and max lines only differ in line width.
    i=0
    defs=""
    plots=""
    set -- $COLORS
    for name in $NAMES; do 
        if [[ $SENSORS == *$i* ]]; then
            defs="$defs DEF:mn$i=$DBASE:$name:MIN"
            defs="$defs DEF:mx$i=$DBASE:$name:MAX"
            defs="$defs CDEF:dif$i=mx$i,mn$i,-"
            if [ "$MNMXPLOT_STYLE" = "area" ]; then
                plots="$plots\"AREA:mn$i\" "
                plots="$plots\"STACK:dif$i#$1:${LABELS[$i]} min-max\" "
            else            
                plots="$plots\"LINE1:mn$i#$1:${LABELS[$i]} min/max\" "
                plots="$plots\"LINE2:mx$i#$1\" "
            fi
        fi
        shift
        let i=$i+1
    done
    let seconds=24*3600*$MNMXPLOT_DAYS
    if [ -n "$verbose" ]; then
        echo "$rrdtool graph $DIAGRAM -s -$seconds \\"
    	for i in $defs; do
    	    echo "  $i \\"
    	done
    	temp=${plots// /_}
    	set -- ${temp//\"_/\" }
    	while [ -n "$2" ]; do
    	    echo "  ${1//_/ } \\"
    	    shift
    	done
    	echo "  ${1//_/ }"
    fi
    echo $rrdtool graph $DIAGRAM -s -$seconds $defs $plots | /bin/sh
    exit 0
fi



if [ "$1" = "-r" ]; then
    record="true"
else    
    cat <<BLOCK_MSG
Please edit the script to match the properties of your Web-IO device, 
then execute:
  $0 -c       [dbase]           to create the RRDtool database
  $0 -r       [dbase] [device]  to start recording values
  $0 -i       [dbase]           to show the most recent values
  $0 -d[days] [dbase] [diagram] [sensors]  to create a .png diagram
  $0 -D[days] [dbase] [diagram] [sensors]  for a min/max diagram
You may add -v before all other parameters for verbose output.
Current default values are:
  dbase=$DBASE
  device=$TGT_DEVICE
  diagram=$DIAGRAM
BLOCK_MSG
    exit 1
fi



# Record sensor values
while [ "$record" = "true" ]; do
    # Get the sensor string, paying attention to the correct
    # character encoding.
	if [ $CSMODE = "utf" ]; then
		reply=`lynx -dump -display_charset=UTF-8 $TGT_DEVICE/Single`
	elif [ $CSMODE = "iso" ]; then
		reply=`lynx -dump $TGT_DEVICE/Single`
	else    
		reply=`lynx -dump $TGT_DEVICE/Single | iconv -f iso-8859-1`
	fi

    # Split the string where the semicolons are.
    reply=`echo $reply | sed -e "s# ##g" | sed -e "s#;# #g"`
    set -- $reply
    
    # In case the first field is an IP address, we are dealing with a
    # reply in extended format, and we have to drop the first two fields.
    if [[ $1 == *.*.*.* ]]; then
        shift 2
    fi
    #echo [$(date +%T)] $*

    # Examine the values and accumulate two strings: The values that we will
    # report and the data source names that they belong to.
    template=""
    values=""
    i=0
    for name in $NAMES; do 
        if [ -z "$1" ]; then
            # no more sensor values
            break
        fi
        # Replace comma by point and remove unit symbols (C, %).
        val=`echo $1 | sed -e "s#,#.#" | sed -e "s#[${DEGSIGN}C%]##g"`
        # Only record a sensor value, if its value appears on the list...
        if [[ $SENSORS == *$i* ]]; then
            # ...and only, if the device reported an actual value, not 
            # just dashes ("----").
            trunc=${val//-/}
            if [ -n "$trunc" ]; then
                #echo "$i: $1 ($val)"
                template=$template:$name
                values=$values:$val
            fi
        fi
        let i=$i+1
        shift
    done

    if [ -n "$verbose" ]; then
        echo $rrdtool update $DBASE -t ${template#:} N$values
    fi    
    $rrdtool update $DBASE -t ${template#:} N$values
    
    # sleep until the next interval is complete
    perl -e "sleep $INTERVAL - time % $INTERVAL"
done
