#!/usr/bin/perl
##########################################################
#
# WeatherMachine - by Ed T. Toton III
#
# http://www.necrobones.com/
#
# Retrives and logs data from WMR-968 Oregon Scientific
# weather stations, and updates Weather Underground
# accordingly.
#
# Uses ideas and some code fragments from similar scripts
# written by the following people:
#
#     Tom Southerland (polldavis.pl, davis_wu_upload.pl)
#     Alan Jackson (WeatherTools, ajackson.org)
#
# This script is being distributed for free. You may use
# any and all of this code as you see fit. 
#
##########################################################

my $version	= '2009.04.02.A';	# Date-based version

my $debug	= 0;			# Controls some debugging features

# Config
my $progname	= 'weathermachine.pl';	# Change accordingly if you rename the program.
my $maindir	= '/var/www/html/weather';	# Make sure this and progname are accurate or
					# HUP signals won't work, plus other problems.
my $admin_email	= 'smoore@teuse.net';	# Admin's e-mail, to send errors
my $mailprog	= '/bin/mail';			# Program for sending e-mail.
my $battwarntime= 21600;			# Battery warning repeat interval (seconds)

my $logdir	= $maindir.'/log';	# Log-file locations
my $statusfile	= $logdir.'/weathermachine.status';
my $logfile	= $logdir.'/weathermachine.log';
my $dailyfile	= $logdir.'/weathermachine.daily';

# Settings for Weather Underground updates. (www.wunderground.com)
my $wu_account	= 'KFLTALLA27';		# Leave blank to disable WU updating. This is the site not a username
my $wu_passwd	= '';
my $WU = "http://weatherstation.wunderground.com/weatherstation/updateweatherstation.php";
my $softwaretype = "weathermachine.pl";

# serial port info
my $port = "/dev/ttyS0";		# Adjust these according to your choice
my $baud = "9600";			# of COM/serial ports. 9600 should be used.

use POSIX qw(strftime);
use strict;

my $quit = 0;
my $lastbattwarn = 0;


##### Daemonize

# Check to see we're not running
my $psfound = 0;
open PS, "/bin/ps awx | /bin/grep $progname |";
while (<PS>) {
        $psfound++ if ((/$progname/) && !(/grep/));
}
close PS;
exit if ($psfound>1);

&daemonize;

##### End daemonize



use Device::SerialPort;
use LWP::UserAgent;
use HTTP::Request;




# create device constructor
my $ob;
$ob = Device::SerialPort->new ($port,1) || die "Can't open $port: $!";
$ob->baudrate($baud)       || die "fail setting baudrate";
$ob->parity("none")       || die "fail setting parity";
$ob->databits(8)   || die "fail setting databits";
$ob->stopbits(1)   || die "fail setting stopbits";
$ob->handshake("none") || die "fail setting handshake";
# timeouts
$ob->read_char_time(50);     # time between chars
$ob->read_const_time(3000); # timeout after 3 seconds / LOOP is sent every 2.5 sec
$ob->write_settings || die "no settings";

my %info = ();
my ($count, $winddir, $windspeed, $windgust, $humidity, $temperature, $barometer, $rain);
my ($dewpt, $tempC, %wuf, $key, $wu_url);




## START PROGRAM ###

my $lasttime = &timecode;
my $lastdate = 0;
my $gotraindata = 0;
my $wrotedaily = 0;
&init;


while (!$quit) {

	# Be a nice daemon
	sleep(1);	

	my ($grabbed, $datagrabbed) = $ob->read(100);

	if ($grabbed) {
		my @data = unpack('C*', $datagrabbed);
		&parse(@data);
	}

	if ($lastdate ne &datecode) {
		$gotraindata = 0;
		$wrotedaily = 0;
	}
	$lastdate = &datecode;

	&update if ($lasttime ne &timecode);
	$lasttime = &timecode;

}

undef $ob;


### END PROGRAM ###


sub myprint {
	print @_ if ($debug);
}

sub update_wu {
	my %wuf = ();
	&myprint("Contacting Weather Underground...");
	my $now = time();

	$wuf{dateutc} = strftime("%Y-%m-%d %H:%M:%S",gmtime($now));
#	$wuf{softwaretype} = $softwaretype;
	$wuf{weather} = "NA";
	$wuf{clouds} = "NA";

	$wuf{dewptf} = sprintf("%.2f",($info{outdoordewpoint}*9/5)+32);
	$wuf{baromin} = sprintf("%.2f",$info{barosea}*0.02953);
	$wuf{tempf} = sprintf("%.2f",($info{outdoortemp}*9/5)+32);
	$wuf{humidity} = $info{outdoorhumidity};
	$wuf{winddir} = $info{windgustdir};
	$wuf{windspeedmph} = sprintf("%.2f",$info{windavgspeed}*9/4);
	$wuf{windgustmph}  = sprintf("%.2f",$info{windgustspeed}*9/4);
	$wuf{rainin} = sprintf("%.2f",$info{rainrate}/25.4);
	
	my $url = $WU."?action=updateraw&ID=".$wu_account."&PASSWORD=".$wu_passwd;
	foreach $key(sort(keys(%wuf))){ $url .= "&".$key."=".$wuf{$key} }
	&myprint("$url\n");
	my $ua = LWP::UserAgent->new;
	my $req = HTTP::Request->new(GET => $url);
	my $resp = $ua->simple_request($req);
	my $resp_data = $resp->content;
	&myprint("$resp_data (done)\n");
}


sub init {
	%info = (windgustdir => 0, windgustspeed => 0, windavgspeed => 0,
		windchill => 0,	rainrate => 0, raintotal => 0, rainyesterday => 0,
		indoortemp => 0, indoorhumidity => 0, indoordewpoint => 0,
		outdoortemp => 0, outdoorhumidity => 0, outdoordewpoint => 0,
		baro => 0, barosea => 0 );
	if (-e $statusfile) {
		open INFILE, "<$statusfile";
		while (<INFILE>) {
			chomp;
			if ($_) {
				@_ = split / /;
				my $timestamp = shift;
				$info{windgustdir} = shift;
				$info{windgustspeed} = shift;
				$info{windavgspeed} = shift;
				$info{windchill} = shift;
				$info{rainrate} = shift;
				$info{raintotal} = shift;
				$info{rainyesterday} = shift;
				$info{indoortemp} = shift;
				$info{indoorhumidity} = shift;
				$info{indoordewpoint} = shift;
				$info{outdoortemp} = shift;
				$info{outdoorhumidity} = shift;
				$info{outdoordewpoint} = shift;
				$info{baro} = shift;
				$info{barosea} = shift;
				$info{weather} = shift;
				$info{resetyear} = shift;
				$info{resetmonth} = shift;
				$info{resetday} = shift;
			}
		}
		close INFILE;
	}

}


sub write_logline {
	my $filename = shift;
	my $append = shift;
	if ($append) {
		open(OUTFILE,">>$filename") or die "Can't write to $filename!\n";
	} else {
		open(OUTFILE,">$filename") or die "Can't write to $filename!\n";
	}
	print OUTFILE &timecode." $info{windgustdir} $info{windgustspeed} $info{windavgspeed} $info{windchill} ";
	print OUTFILE "$info{rainrate} $info{raintotal} $info{rainyesterday} ";
	print OUTFILE "$info{indoortemp} $info{indoorhumidity} $info{indoordewpoint} ";
	print OUTFILE "$info{outdoortemp} $info{outdoorhumidity} $info{outdoordewpoint} ";
	print OUTFILE "$info{baro} $info{barosea} $info{weather} ";
	print OUTFILE "$info{resetyear} $info{resetmonth} $info{resetday}\n";
	close OUTFILE;
}



# Executed once a minute
sub update {
	&write_logline("$statusfile",0);
	&myprint(&timecode." Status written to $statusfile\n");

	# Write to log once every 2 minutes
	my $timestring = &timecode;
	if ($timestring =~ /[02468]$/) {
		my $filename = "$logfile.".&monthcode;
		&write_logline($filename,1);
		&myprint(&timecode." Status written to $filename\n");

		if ($gotraindata && !$wrotedaily) {
			&check_daily;
		}
	}
	if ($timestring =~ /[05]$/) {
		&update_wu if ($wu_account);
	}

	&check_batteries;

}



sub check_batteries {
	my $warn	= '';

	foreach my $key (keys %info) {
		if ($key eq 'mainbattstatus') {
			# Do nothing
		} elsif ($key eq 'mainbatt') {
			$warn .= "Console battery low!\n" if (($info{$key} & 0x80) > 0);
		} elsif ($key =~ /batt/) {
			$warn .= "$key = $info{$key}\n" if ($info{$key} > 6);
		}
	}

	if ($warn && ($lastbattwarn < time - $battwarntime)) {
		$lastbattwarn = time;

		open MAILOUT, "| $mailprog -s \"WeatherMachine Battery notification\" $admin_email";
		print MAILOUT $warn;
		close MAILOUT;
	}
}





sub printdata {
	my @data = @_;

        foreach my $data (@data) {
                &myprint(uc(sprintf("%02x",$data))." ");
        }
        &myprint("\n");
}





sub timecode {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
	$year += 1900;
	$mon++;
	return sprintf("%04u%02u%02u%02u%02u",$year,$mon,$mday,$hour,$min);
}



sub datecode {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
	$year += 1900;
	$mon++;
	return sprintf("%04u%02u%02u",$year,$mon,$mday);
}

sub monthcode {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
	$year += 1900;
	$mon++;
	return sprintf("%04u%02u",$year,$mon);
}


sub yearcode {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
	$year += 1900;
	$mon++;
	return sprintf("%04u",$year);
}


sub yesteryear {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time-86400);
	$year += 1900;
	$mon++;

	return sprintf("%04u",$year);
}


sub yesterdaymonth {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time-86400);
	$year += 1900;
	$mon++;

	return sprintf("%04u%02u",$year,$mon);
}


sub yesterday {
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time-86400);
	$year += 1900;
	$mon++;

	return sprintf("%04u%02u%02u",$year,$mon,$mday);
}



sub parse {
	my @data = @_;
	my $i = 0;

	my ($winddir,$windspeedmph,$windgustmph,$humidity,$tempf,$baromin,$dewptf,$rainin,$dateutc,$softwaretype,$weather,$clouds);

        &printdata(@data) if ($debug);
	
	while (($i < @data) && ($data[$i+1] == 0xFF)) {

		my $temp1 = $data[$i++];
		my $temp2 = $data[$i++];
		my $group = $data[$i++];
		my $found = 0;

		#print sprintf("%02x %02x%02x",$group,$temp1,$temp2)."\n";

		if (($group == 0x00) && !$found) {  # Anemometer
			my $batt = $data[$i++];
			$info{anemometerbatt} = ($batt & 0xF0)/16;
			my $tmp1  = $data[$i++];
			my $tmp2 = $data[$i++];
			$info{windgustdir} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp1)+0;
			my $tmp3  = $data[$i++];
			$info{windgustspeed} = sprintf("%02x%02x",$tmp3,$tmp2 & 0xF0)/100;
			my $tmp4 = $data[$i++];
			my $tmp5 = $data[$i++];
			$info{windavgspeed} = sprintf("%02x%02x",$tmp5 & 0x0F,$tmp4)/10;
			my $tmp6  = $data[$i++];
			$info{windchill} = sprintf("%x", $tmp6);
			&myprint("WindGustDir $info{windgustdir}, WindGustSpeed $info{windgustspeed}, WindAvgSpeed $info{windavgspeed}, WindChill $info{windchill}\n");
			my $crc = $data[$i++];
			$found = 1;
		}


		if (($group == 0x01) && !$found) {  # Rain Gauge
			my $batt = $data[$i++];
			$info{rainbatt} = ($batt & 0xF0) >> 4;

			my $tmp  = $data[$i++];
			my $tmp2 = $data[$i++];
			$info{rainrate} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp)+0;
			my $tmp  = $data[$i++];
			my $tmp2 = $data[$i++];
			$info{raintotal} = sprintf("%02x%02x",$tmp2,$tmp)+0;
			my $tmp  = $data[$i++];
			my $tmp2 = $data[$i++];
			$info{rainyesterday} = sprintf("%02x%02x",$tmp2,$tmp)+0;
			$info{resetminute}   = $data[$i++];
			$info{resethour}     = $data[$i++];
			$info{resetday}      = $data[$i++];
			$info{resetmonth}    = $data[$i++];
			$info{resetyear}     = $data[$i++]+2000;
			&myprint("RainRate $info{rainrate}, RainTotal $info{raintotal}, RainYesterday $info{rainyesterday}, RainReset $info{resethour}:$info{resetminute} $info{resetday}/$info{resetmonth}/$info{resetyear}\n");
			my $crc = $data[$i++];
			$found = 1;
			$gotraindata = 1;
		}


		if (($group == 0x02) && !$found) {  # Extra Sensors
			# We don't want extra sensor data, so dump it.
			$i += 6;
			$found = 1;
		}


		if (($group == 0x03) && !$found) {  # Outdoor Temp, Humidity, Dewpoint
			my $batt = $data[$i++];
			$info{outdoorthermbatt} = ($batt & 0xF0) >> 4;
			my $tmp  = $data[$i++];
			my $tmp2 = $data[$i++];
			$info{outdoortemp} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp)/10;
			$info{outdoortemp} = '-'.$info{outdoortemp} if ($tmp2 & 0x80);
			$info{outdoorhumidity} = sprintf("%x", $data[$i++]);
			$info{outdoordewpoint} = sprintf("%x", $data[$i++]);
			&myprint("OutdoorTemp  $info{outdoortemp}, OutdoorHumidity $info{outdoorhumidity}, OutdoorDewPoint $info{outdoordewpoint}\n");
			my $crc = $data[$i++];
			$found = 1;
		}


 if (($group == 0x05) && !$found) {  # Indoor Temp, Humidity, Dewpoint, Barometer
                        my $batt = $data[$i++];
                        $info{indoorthermbatt} = ($batt & 0xF0) >> 4;
                        my $tmp  = $data[$i++];
                        my $tmp2 = $data[$i++];
                        $info{indoortemp} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp)/10;
                        $info{indoortemp} = '-'.$info{indoortemp} if ($tmp2 & 0x80);
                        $info{indoorhumidity} = sprintf("%x", $data[$i++]);
                        $info{indoordewpoint} = sprintf("%x", $data[$i++]);
                        my $tmp  = $data[$i++];
                        my $tmp2 = $data[$i++];
                        $info{baro} = hex(sprintf("%02x%02x",$tmp2 & 0x01,$tmp))+795;
                        $info{tendency} = $data[$i++];
                        $info{weather} = "Clear" if ($info{tendency} == 0x0C);
                        $info{weather} = "PartlyCloudy" if ($info{tendency} == 0x06);
                        $info{weather} = "Cloudy" if ($info{tendency} == 0x02);
                        $info{weather} = "Rain" if ($info{tendency} == 0x03);
                        $info{weather} = "Unknown" if (!$info{weather});
                        my $tmp  = $data[$i++];
                        my $tmp2 = $data[$i++];
                        $info{barosea} = (sprintf("%02x%02x",$tmp2,$tmp)/10)+$info{baro}-795;
                        my $crc = $data[$i++];
                        &myprint("IndoorTemp $info{indoortemp}, IndoorHumidity $info{indoorhumidity}, IndoorDewPoint $info{indoordewpoint}, BaroPressure $info{baro}, BaroSea $info{barosea}, CurrentWeather $info{weather}\n");
                        $found = 1;
                }

 if (($group == 0x06) && !$found) {  # Indoor Temp, Humidity, Dewpoint, Barometer
                        my $batt = $data[$i++];
                        $info{indoorthermbatt} = ($batt & 0xF0)/16;
                        my $tmp  = $data[$i++];
                        my $tmp2 = $data[$i++];
                        $info{indoortemp} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp)/10;
                        $info{indoortemp} = '-'.$info{indoortemp} if ($tmp2 & 0x80);
                        $info{indoorhumidity} = sprintf("%x", $data[$i++]);
                        $info{indoordewpoint} = sprintf("%x", $data[$i++]);
                        my $tmp  = $data[$i++];
                        my $tmp2 = $data[$i++];
                        $info{baro} = hex(sprintf("%02x%02x",$tmp2 & 0x01,$tmp))+600;
#                        $info{tendency} = $data[$i++];
			 $info{tendency} = ($tmp2 & 0xF0) >> 4;

			&myprint("Tendancy $info{tendency}\n");
                        $info{weather} = "Clear" if ($info{tendency} == 0x0C);
                        $info{weather} = "PartlyCloudy" if ($info{tendency} == 0x06);
                        $info{weather} = "Cloudy" if ($info{tendency} == 0x02);
                        $info{weather} = "Rain" if ($info{tendency} == 0x03);
                        $info{weather} = "Unknown" if (!$info{weather});
			 my $tmp  = $data[$i++];

                        my $tmp  = $data[$i++];
                        my $tmp2 = $data[$i++];
                        my $tmp3 = $data[$i++];
                        $info{barosea} = sprintf("%02x%02x",$tmp2,$tmp)+$info{baro}-600;
                        my $crc = $data[$i++];
                        &myprint("IndoorTemp $info{indoortemp}, IndoorHumidity $info{indoorhumidity}, IndoorDewPoint $info{indoordewpoint}, BaroPressure $info{baro}, BaroSea $info{barosea}, CurrentWeather $info{weather} Tend: $info{tendency}  \n");
                        $found = 1;
                }


		if (($group == 0x0E) && !$found) {  # Sequence Number
			my $tmp = $data[$i++];
			$info{mainbattstatus} = $tmp & 0xF0;
			$info{chimeminute} = $tmp & 0x7F;
			my $crc = $data[$i++];
			$found = 1;
		}


		if (($group == 0x0F) && !$found) {  # Hourly Status Report
			$info{mainbatt} = $data[$i++];
			$info{hour} = sprintf("%02x",$data[$i++]);
			$info{day} = sprintf("%02x",$data[$i++]);
			$info{month} = sprintf("%02x",$data[$i++]);
			$info{year} = sprintf("%02x",$data[$i++]);
			my $crc = $data[$i++];
			$found = 1;
		}
		
		# Sanity Check
		#$info{outdoortemp} = 105 if ($info{outdoortemp} > 105);


	}


}



sub calculate_daily {
	open INFILE, "<$logfile".'.'.&yesterdaymonth or return;
	my @lines = <INFILE>;
	close INFILE;

	my $yesterday = &yesterday;
	my ($temphi,$templo,$maxwind,$maxgust,$rainhi,$rain,$humidhi,$humidlo,$dewhi,$dewlo,$barohi,$barolo,$seahi,$sealo) = (0,999999,0,0,0,0,0,999999,0,999999,0,999999,0,999999);
	my ($avgtemp,$avgwind,$avggust,$avgrain,$avghumid,$avgdew,$avgbaro,$avgbarosea) = (0,0,0,0,0,0,0,0);

	my $i = 0;
	foreach my $line (@lines) {
		chomp $line;
		my ($timestamp,$windgustdir,$windgustspeed,$windavgspeed,
		   $windchill,$rainrate,$raintotal,$rainyesterday,$indoortemp,
		   $indoorhumidity,$indoordewpoint,$outdoortemp,$outdoorhumidity,
		   $outdoordewpoint,$baro,$barosea) = split / /,$line;
		
		if ($timestamp =~ /^$yesterday/) {
			$temphi  = $outdoortemp if ($outdoortemp > $temphi);
			$templo  = $outdoortemp if ($outdoortemp < $templo);
			$dewhi   = $outdoordewpoint if ($outdoordewpoint > $dewhi);
			$dewlo   = $outdoordewpoint if ($outdoordewpoint < $dewlo);
			$barohi  = $baro if ($baro > $barohi);
			$barolo  = $baro if ($baro < $barolo);
			$seahi   = $barosea if ($barosea > $seahi);
			$sealo   = $barosea if ($barosea < $sealo);
			$maxwind = $windavgspeed  if ($windavgspeed > $maxwind);
			$maxgust = $windgustspeed if ($windgustspeed > $maxgust);
			$humidhi = $outdoorhumidity if ($outdoorhumidity > $humidhi);
			$humidlo = $outdoorhumidity if ($outdoorhumidity < $humidlo);
			$rainhi  = $rainrate if ($rainrate > $rainhi);
			$i++;
			$avgtemp    += $outdoortemp;
			$avgwind    += $windavgspeed;
			$avggust    += $windgustspeed;
			$avgrain    += $rainrate;
			$avghumid   += $outdoorhumidity;
			$avgdew     += $outdoordewpoint;
			$avgbaro    += $baro;
			$avgbarosea += $barosea;
		}
	}
	if ($i) {
		$avgtemp /= $i;
		$avgwind /= $i;
		$avggust /= $i;
		$avgrain /= $i;
		$avghumid /= $i;
		$avgdew /= $i;
		$avgbaro /= $i;
		$avgbarosea /= $i;
	}
	my $totalrain = $info{rainyesterday};

	open OUTFILE, ">>$dailyfile".'.'.&yesteryear;
	print OUTFILE $yesterday.sprintf(" %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f %.02f",$avgtemp,$avgdew,$avghumid,$avgwind,$avggust,$avgbaro,$avgbarosea,$avgrain,$totalrain,$temphi,$templo,$dewhi,$dewlo,$humidhi,$humidlo,$maxwind,$maxgust,$barohi,$barolo,$seahi,$sealo,$rainhi)."\n";
	close OUTFILE;

	&myprint(&timecode." Wrote daily stats for $yesterday to $dailyfile".'.'.&yesteryear."\n");
}


sub check_daily {

	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
	$year += 1900;
	$mon++;
	return 0 if ($hour < 3);

	my $found = 0;
	my $yesterday = &yesterday;
	my $filename = "$dailyfile".'.'.&yesteryear;

	if (-e $filename) {
		open DAILY, "<$filename";
		my @lines = <DAILY>;
		foreach my $line (@lines) {
			if ($line =~ /$yesterday/) {
				$found = 1;
				last;
			}
		}
		close DAILY;
	}

	if (!$found && $gotraindata && !$wrotedaily) {
		$wrotedaily = 1;
		&calculate_daily;
		$gotraindata = 0;
	}
}



sub daemonize {
  ##### Daemonize

  #open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  #open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
  #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";

  # Fork
  my $pid = fork;
  exit if $pid;
  die "Couldn't fork: $!" unless defined($pid);

  POSIX::setsid() or die "Can't start new session: $!";

  $SIG{INT} = $SIG{TERM} = \&sig_handler;
  $SIG{PIPE} = 'IGNORE';
  $SIG{HUP} = \&hup_handler;

  ##### End daemonize
}


sub sig_handler {
        $quit = 1;
}
sub hup_handler {
        exec($maindir.'/'.$progname,'') or die "Couldn't restart: $!\n";
}

