#!@PERLPATH@
#
# Ultimeter 2000 data logger
# Copyright 1999 by Jonathan Bradshaw, N9OXE (n9oxe@arrl.net)
# $Id: log_u2000.in,v 1.1 2001/12/06 06:58:25 kg9ae Exp $
#
# Licensed under the GPL (see LICENSE file for details)
# Uses weather protocol information from PeetBros (www.peetbros.com)
#
# In addition to all the fields provided by the weather station, the
# following fields are calculated:
#
# CalcWindAvg       - Avg wind speed of last minute's readings
# CalcRainLastHour  - Rain in the last 60 minutes
# CalcRainLast24hr  - Rain in the last 24 hours
# CalcRainMonth     - Rain so far this month
# CalcHeatIndex     - Heat index if humidity sensor and temp > 72F
#
# Sets ultimeter clock to system time when started and requests 
# streaming complete record mode.
#
# Requirements:
#	Perl 5.004 or later
#	Ultimeter 2000
#	Modules POSIX and Sys::Syslog
#
# CHANGELOG:
# 1.1   Fixed temperature when it goes below 0F
#

require 5.004;

use strict;
#use Sys::Syslog;
use POSIX qw(setsid mktime strftime);

# Autoconf Values
my $LOGDIR = "@prefix@/log";
my $STATEDIR = "@prefix@/log/run";

#
# Set the PORT to the communications port the Ultimeter is connected to.
# For unix this will be /dev/ttySn where n is the port number
# For Windows this will be COM1, COM2 etc.
#
my $PORT = "/dev/ttyS1";
#
# Set DATAFILE to the file that will contain the latest weather data
# If you don't want a log, leave it blank.
#
my $DATAFILE = $LOGDIR."/u2000.dat";
#
# Set HISTORYFILE to the file that will contain a history of data samples
# If you don't want a log, leave it blank. LOGEVERY controls how many
# samples are written (you get approx 9 records every minute)
#
# File format is CSV and close to the data logging mode of the Ultimeter
#
my $LOGFILE = $LOGDIR."/u2000.log";
my $LOGEVERY = (5 * 9);	# Approx every 5 minutes (9 per minute)
#
# STATEFILE is used to save and load state information each time the
# program is run.
#
my $STATEFILE = $STATEDIR."/log_u2000.state";


# Global variables for calculations
my (@WindValues, @RainLastHour, @RainLast24hr, @RainMonth, $counter);

#
# Convert number of days into date. We find the number of seconds for
# Midnight on January 1st and add the given number of days and minutes to it.
# NOTE! Do not change the format without fixing the calculation code below!
#
sub days2date {
  my ($days) = @_;
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
  $year-- if ($days > $yday); # Should take care of dates from last year
  return strftime("%m/%d/%Y", localtime(mktime(0,0,0,1,0,$year,0,0,0) + (86400 * $days)));
}

sub mins2time {
  my ($minutes) = @_;
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
  return strftime("%H:%M", localtime(mktime(0,0,0,$mday,$mon,$year,0,0,0) + ($minutes * 60)));
}


#
# Takes the current weather data and returns rain amount for last hour
# If rain counter gets set to zero it zero's out the history
# Returns an empty string if the data is not yet available
#
sub calcrainlasthour {
  my ($data) = @_;
  my ($delta);

  my $min = substr($data->{Time},3,2);
  $RainLastHour[$min] = $data->{RainTotal};

  $min++; $min=0 if $min==60;
  if (!$RainLastHour[$min]) {
    return "";
  }

  $delta = $data->{RainTotal} - $RainLastHour[$min];
  if ($delta < 0) {
    # Someone appears to have reset the rain counter
    @RainLastHour = ();
    return "";
  } else {
    return sprintf("%.2f", $delta);
  }
}


#
# Takes the current weather data and returns rain amount for last 24hrs
# If rain counter gets set to zero it zero's out the history
# Returns an empty string if the data is not yet available
#
sub calcrainlast24hr {
  my ($data) = @_;
  my ($delta);

  my $hour = substr($data->{Time},0,2);
  $RainLast24hr[$hour] = $data->{RainTotal};

  $hour++; $hour=0 if $hour==24;
  if (!$RainLast24hr[$hour]) {
    return "";
  }

  $delta = $data->{RainTotal} - $RainLast24hr[$hour];
  if ($delta < 0) {
    # Someone appears to have reset the rain counter
    @RainLast24hr = ();
    return "";
  } else {
    return sprintf("%.2f", $delta);
  }
}


#
# Takes the current weather data and returns rain amount for month to date
# Daily rainfall amounts are used so the rain gauge can be reset
#
sub calcrainformonth {
  my ($data) = @_;
  my ($i, $total);

  my $mday = substr($data->{Date},3,2);
  if ($mday == 1) {   # Reset data on the first day of the month
    @RainMonth = ();
  }

  $RainMonth[$mday] = $data->{RainTotalToday};
  for $i (@RainMonth) {
    $total += $i;
  }
  return sprintf("%.2f", $total);
}


#
# Takes the current weather data and returns the average wind speed
# over the last 9 readings which should be about a minute.
#
sub calcwindavg {
  # At 2400bps there should be about 9 datasets per minute
  my ($data) = @_;
  my ($i, $avg, $count);
  @WindValues = (@WindValues[1..8], $data->{WindSpeed});
  for $i (@WindValues) {
    if ($i) { 
      $avg += $i;
      $count++;
    }
  }
  return sprintf("%.1f", $avg / $count);
}


#
# Writes the current rain datasets to a file
#
sub savestate {
  my $i;

  if ($STATEFILE) {
    open(STATE, ">$STATEFILE");
    for $i (@RainLast24hr) {
      print STATE "$i,";
    }
    print STATE "\n";
    for $i (@RainLastHour) {
      print STATE "$i,";
    }
    print STATE "\n";
    for $i (@RainMonth) {
      print STATE "$i,";
    }
    print STATE "\n";
    close(STATE);
  }
}


#
# Reads the rain datasets from a file
#
sub loadstate {
  return unless ( -f $STATEFILE );
  #syslog('info', 'Reading previous state from %s', $STATEFILE);
  open(STATE, "<$STATEFILE");
  @RainLast24hr = split(/,/, substr(<STATE>,0,-1));
  @RainLastHour = split(/,/, substr(<STATE>,0,-1));
  @RainMonth = split(/,/, substr(<STATE>,0,-1));
  close(STATE);
}


#
# Sets ultimeter clock and puts Ultimeter into packet dump mode
#
sub initu2k {
  my ($daynum, $minnum);
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
  $daynum = sprintf("%04d", $yday);
  $minnum = sprintf("%04d", ($hour*60) + $min);

  system("stty 2400 clocal raw < $PORT");
  open(ULTIMETER, ">$PORT");
  select((select(ULTIMETER), $| = 1)[0]); # No buffering of writes
  #syslog('info', 'Sending clock set command >A%s', $daynum . $minnum);
  print ULTIMETER ">A" . $daynum . $minnum . "\n";
  print ULTIMETER ">K\n";  # Set complete record mode
  close(ULTIMETER);
}


#
# Calculates heat index. Requires temperature above 72F and humidity sensor
#
sub calcheatindex {
  my ($data) = @_;
  my ($temp, $rh) = ($data->{OutdoorTemp}, $data->{OutdoorHumidity});

  if ($rh > 0 && $temp >= 72) {
    return sprintf("%.2f", 
    -42.379 + 2.04901523*$temp + 10.14333127*$rh - 0.22475541*$temp*$rh -
    6.83783e-03*$temp**2 - 5.481717e-02*$rh**2 + 1.22874e-03*$temp**2*$rh +
    8.5282e-04*$temp*$rh**2 - 1.99e-06*$temp**2*$rh**2);
  } else {
    return;
  }
}


#
# Decode data stream from Ultimeter. Units are converted to American format.
# Decoded using specs from http://www.peetbros.com/peetbrosnofram/faqs.htm
#
sub decodewx {
  sub dehex {
    my ($value) = @_;
    $value = hex($value);
    return ($value > 32768 ? $value-65536 : $value);
  }

  my ($data) = @_;
  $data =~ s/-/0/g; # Convert any dashes to zeros

  return {
    # Average 3 wind speed values given in data stream
    WindSpeed => sprintf("%.1f", (
                   dehex(substr($data,4,4)) +
                   dehex(substr($data,136,4)) +
                   dehex(substr($data,284,4)) ) / 3 * 0.062),

    # Average of 3 wind direction values given in data stream
    WindDirection => sprintf("%d", (
                   dehex(substr($data,10,2)) +
                   dehex(substr($data,142,2)) +
                   dehex(substr($data,290,2)) ) / 3 * 1.41176),

    WindPeakSpeed5Min => sprintf("%.1f", dehex(substr($data,12,4)) * 0.062),
    WindPeakDirection5Min => sprintf("%d", dehex(substr($data,18,2)) * 1.41176),
    WindChill => sprintf("%.1f", dehex(substr($data,20,4)) / 10),
    OutdoorTemp => sprintf("%.1f", dehex(substr($data,24,4)) / 10),
    RainTotalToday => sprintf("%.2f", dehex(substr($data,28,4)) / 100),

    Barometer => sprintf("%.2f", dehex(substr($data,32,4)) * 0.002953),
    BarometerDelta => sprintf("%.2f",dehex(substr($data,36,4))*0.002953),
    BarometerCorrectionLSW => substr($data,40,4),
    BarometerCorrectionMSW => substr($data,44,4),

    IndoorTemp => sprintf("%.1f", dehex(substr($data,48,4)) / 10),
    OutdoorHumidity => sprintf("%.1f", dehex(substr($data,52,4)) / 10),
    IndoorHumidity => sprintf("%.1f", dehex(substr($data,56,4)) / 10),
    DewPoint => sprintf("%.1f", dehex(substr($data,60,4)) / 10),
    Date => days2date(dehex(substr($data,64,4))),
    Time => mins2time(dehex(substr($data,68,4))),

    LowWindChillToday => sprintf("%.1f", dehex(substr($data,72,4)) / 10),
    LowWindChillTodayTime => mins2time(dehex(substr($data,76,4))),
    LowWindChillYesterday => sprintf("%.1f", dehex(substr($data,80,4)) / 10),
    LowWindChillYesterdayTime => mins2time(dehex(substr($data,84,4))),
    LowWindChillDate => days2date(dehex(substr($data,88,4))),
    LowWindChill => sprintf("%.1f", dehex(substr($data,92,4)) / 10),
    LowWindChillTime => mins2time(dehex(substr($data,96,4))),

    LowOutdoorTempToday => sprintf("%.1f", dehex(substr($data,100,4)) / 10),
    LowOutdoorTempTodayTime => mins2time(dehex(substr($data,104,4))),
    LowOutdoorTempYesterday => sprintf("%.1f", dehex(substr($data,108,4)) / 10),
    LowOutdoorTempYesterdayTime => mins2time(dehex(substr($data,112,4))),
    LowOutdoorTempDate => days2date(dehex(substr($data,116,4))),
    LowOutdoorTemp => sprintf("%.1f", dehex(substr($data,120,4)) / 10),
    LowOutdoorTempTime => mins2time(dehex(substr($data,124,4))),

    LowBarometerToday => sprintf("%.2f", dehex(substr($data,128,4)) * 0.002953),
    LowBarometerTodayTime => mins2time(dehex(substr($data,132,4))),
    # skip 8 bytes (136-144) for wind speed and direction data
    LowBarometerYesterday => sprintf("%.2f",dehex(substr($data,144,4))*0.002953),
    LowBarometerYesterdayTime => mins2time(dehex(substr($data,148,4))),
    LowBarometerDate => days2date(dehex(substr($data,152,4))),
    LowBarometer => sprintf("%.2f", dehex(substr($data,156,4)) * 0.002953),
    LowBarometerTime => mins2time(dehex(substr($data,160,4))),

    LowIndoorTempToday => sprintf("%.1f", dehex(substr($data,164,4)) / 10),
    LowIndoorTempTodayTime => mins2time(dehex(substr($data,168,4))),
    LowIndoorTempYesterday => sprintf("%.1f", dehex(substr($data,172,4)) / 10),
    LowIndoorTempYesterdayTime => mins2time(dehex(substr($data,176,4))),
    LowIndoorTempDate => days2date(dehex(substr($data,180,4))),
    LowIndoorTemp => sprintf("%.1f", dehex(substr($data,184,4)) / 10),
    LowIndoorTempTime => mins2time(dehex(substr($data,188,4))),

    LowOutdoorHumidityToday => sprintf("%.1f", dehex(substr($data,192,4)) / 10),
    LowOutdoorHumidityTodayTime => mins2time(dehex(substr($data,196,4))),
    LowOutdoorHumidityYesterday => sprintf("%.1f",dehex(substr($data,200,4))/10),
    LowOutdoorHumidityYesterdayTime => mins2time(dehex(substr($data,204,4))),
    LowOutdoorHumidityDate => days2date(dehex(substr($data,208,4))),
    LowOutdoorHumidity => sprintf("%.1f", dehex(substr($data,212,4)) / 10),
    LowOutdoorHumidityTime => mins2time(dehex(substr($data,216,4))),

    LowIndoorHumidityToday => sprintf("%.1f", dehex(substr($data,220,4)) / 10),
    LowIndoorHumidityTodayTime => mins2time(dehex(substr($data,224,4))),
    LowIndoorHumidityYesterday => sprintf("%.1f",dehex(substr($data,228,4)) / 10),
    LowIndoorHumidityYesterdayTime => mins2time(dehex(substr($data,232,4))),
    LowIndoorHumidityDate => days2date(dehex(substr($data,236,4))),
    LowIndoorHumidity => sprintf("%.1f", dehex(substr($data,240,4)) / 10),
    LowIndoorHumidityTime => mins2time(dehex(substr($data,244,4))),

    HighWindSpeedToday => sprintf("%.1f", dehex(substr($data,248,4)) * 0.062),
    HighWindSpeedTodayTime => mins2time(dehex(substr($data,252,4))),
    HighWindSpeedYesterday => sprintf("%.1f", dehex(substr($data,256,4)) * 0.062),
    HighWindSpeedYesterdayTime => mins2time(dehex(substr($data,260,4))),
    HighWindSpeedDate => days2date(dehex(substr($data,264,4))),
    HighWindSpeed => sprintf("%.1f", dehex(substr($data,268,4)) * 0.062),
    HighWindSpeedTime => mins2time(dehex(substr($data,272,4))),

    HighOutdoorTempToday => sprintf("%.1f", dehex(substr($data,276,4)) / 10),
    HighOutdoorTempTodayTime => mins2time(dehex(substr($data,280,4))),
    # skip 8 bytes (284-292) for wind speed and direction data
    HighOutdoorTempYesterday => sprintf("%.1f", dehex(substr($data,292,4)) / 10),
    HighOutdoorTempYesterdayTime => mins2time(dehex(substr($data,296,4))),
    HighOutdoorTempDate => days2date(dehex(substr($data,300,4))),
    HighOutdoorTemp => sprintf("%.1f", dehex(substr($data,304,4)) / 10),
    HighOutdoorTempTime => mins2time(dehex(substr($data,308,4))),

    HighBarometerToday => sprintf("%.2f", dehex(substr($data,312,4)) * 0.002953),
    HighBarometerTodayTime => mins2time(dehex(substr($data,316,4))),
    HighBarometerYesterday => sprintf("%.2f",dehex(substr($data,320,4))*0.002953),
    HighBarometerYesterdayTime => mins2time(dehex(substr($data,324,4))),
    HighBarometerDate => days2date(dehex(substr($data,328,4))),
    HighBarometer => sprintf("%.2f", dehex(substr($data,332,4)) * 0.002953),
    HighBarometerTime => mins2time(dehex(substr($data,336,4))),

    HighIndoorTempToday => sprintf("%.1f", dehex(substr($data,340,4)) / 10),
    HighIndoorTempTodayTime => mins2time(dehex(substr($data,344,4))),
    HighIndoorTempYesterday => sprintf("%.1f", dehex(substr($data,348,4)) / 10),
    HighIndoorTempYesterdayTime => mins2time(dehex(substr($data,352,4))),
    HighIndoorTempDate => days2date(dehex(substr($data,356,4))),
    HighIndoorTemp => sprintf("%.1f", dehex(substr($data,360,4)) / 10),
    HighIndoorTempTime => mins2time(dehex(substr($data,364,4))),

    HighOutdoorHumidityToday => sprintf("%.1f", dehex(substr($data,368,4)) / 10),
    HighOutdoorHumidityTodayTime => mins2time(dehex(substr($data,372,4))),
    HighOutdoorHumidityYesterday => sprintf("%.1f",dehex(substr($data,376,4))/10),
    HighOutdoorHumidityYesterdayTime => mins2time(dehex(substr($data,380,4))),
    HighOutdoorHumidityDate => days2date(dehex(substr($data,384,4))),
    HighOutdoorHumidity => sprintf("%.1f", dehex(substr($data,388,4)) / 10),
    HighOutdoorHumidityTime => mins2time(dehex(substr($data,392,4))),

    HighIndoorHumidityToday => sprintf("%.1f", dehex(substr($data,396,4)) / 10),
    HighIndoorHumidityTodayTime => mins2time(dehex(substr($data,400,4))),
    HighIndoorHumidityYesterday => sprintf("%.1f", dehex(substr($data,404,4))/10),
    HighIndoorHumidityYesterdayTime => mins2time(dehex(substr($data,408,4))),
    HighIndoorHumidityDate => days2date(dehex(substr($data,412,4))),
    HighIndoorHumidity => sprintf("%.1f", dehex(substr($data,416,4)) / 10),
    HighIndoorHumidityTime => mins2time(dehex(substr($data,420,4))),

    RainTotalYesterday => sprintf("%.2f", dehex(substr($data,424,4)) / 100),
    RainTotalDate => days2date(dehex(substr($data,428,4))),
    RainTotal => sprintf("%.2f", dehex(substr($data,432,4)) / 100),

    LeapYear => substr($data,436,4),
    WDCF => dehex(substr($data,440,4)),

    HighWindDirectionYesterday =>sprintf("%d",dehex(substr($data,444,2))*1.41176),
    HighWindDirectionToday => sprintf("%d", dehex(substr($data,446,2)) * 1.41176),
    # Spare (2 bytes)
    HighWindDirection => sprintf("%d",dehex(substr($data,448,2)) * 1.41176),
  };
}


#
# Write DATAFILE to disk. The output file contains rows of "Field: Value"
# type format.
#
sub writedatafile {
  my ($data) = @_;

  my ($item);
  if ($DATAFILE) {
    open(WXDATFILE,">$DATAFILE");
    foreach $item (sort(keys %$data)) {
      print WXDATFILE "$item: ", $data->{$item}, "\n";
    }
    close(WXDATFILE);
  }
}


#
# Write LOGFILE to disk. The output file is appended to each time and
# contains comma seperated values
#
sub writelogfile {
  my ($data) = @_;

  if ($LOGFILE && ($counter == 0 || ($counter % $LOGEVERY) == 0)) {
    open(WXLOGFILE,">>$LOGFILE");
    print WXLOGFILE $data->{WindSpeed}, ",";
    print WXLOGFILE $data->{WindDirection}, ",";
    print WXLOGFILE $data->{OutdoorTemp}, ",";
    print WXLOGFILE $data->{RainTotal}, ",";
    print WXLOGFILE $data->{Barometer}, ",";
    print WXLOGFILE $data->{IndoorTemp}, ",";
    print WXLOGFILE $data->{OutdoorHumidity}, ",";
    print WXLOGFILE $data->{Date}, ",";
    print WXLOGFILE $data->{Time}, ",";
    print WXLOGFILE $data->{RainTotalToday}, ",";
    print WXLOGFILE $data->{CalcWindAvg1Min}, "\n";
    close(WXLOGFILE);
  }
  $counter++;
}


##############################################################################
# Main Code
##############################################################################
fork && exit;
setsid();

#Sys::Syslog::setlogsock('unix');
#openlog('datalogger', 'pid', 'daemon');
#syslog('notice', 'Ultimeter datalogger starting on %s', $PORT);

initu2k();
loadstate();

open(ULTIMETER, "<$PORT");
while (<ULTIMETER>) {
  chop;
  if (/^&CR&/ && length >= 457) {
    my $data = &decodewx($_);
    $data->{CalcWindAvg1Min} = calcwindavg($data);
    $data->{CalcRainLastHour} = calcrainlasthour($data);
    $data->{CalcRainLast24hr} = calcrainlast24hr($data);
    $data->{CalcRainThisMonth} = calcrainformonth($data);
    $data->{CalcHeatIndex} = calcheatindex($data);
    writedatafile($data);
    writelogfile($data);
    savestate();
  } else {
    #syslog('warning', 'Invalid data stream detected');
  }
}
close(ULTIMETER);
#syslog('notice', 'Ultimeter datalogger stopped');
closelog();

# End
