#!/usr/bin/perl

# http://blog.dest-unreach.be/2009/01/17/reverse-engineering-the-oregon-wmr928nx-weather-station

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use warnings;
use IPC::Open2;

my $debug = 1;
my $fileprefix = "/var/lib/rrdtool/oregon";

sub verify_checksum($) {
# calculates the checksum 8 of the given string argument
	my @bytes = unpack("C*", $_[0]);
	my $should_be_sum = pop @bytes;
	my $sum = -2; # header
	foreach(@bytes) {
		$sum += $_;
		$sum %= 2**8;
	}
	return ($sum == $should_be_sum);
}

sub bits($$$) {
	# vec($$$) variant
	my ($expr, $offset, $bits) = @_;
	return unpack("N", pack("B*", substr("0"x32 . substr(unpack("B*", $expr), $offset, $bits), -32)));
}

sub parse_raw($$) {
	my ($type, $msg) = @_;
	print STDERR "RAW: ", unpack("H2", $type), ": ", unpack("H*", $msg), "\n" if $debug;
	return { "raw" => $msg };
}

sub parse_wind($$) {
	my ($type, $msg) = @_;

	my $battery = bits($msg, 0, 4);
	#my undef = bits($msg, 4, 4);
	my $gustdir = bits($msg, 20, 4) . bits($msg, 8, 4) . bits($msg, 12, 4);
	my $gustspeed = bits($msg, 24, 4) . bits($msg, 28, 4) . "." . bits($msg, 16, 4);
	my $avgspeed = bits($msg, 44, 4) . bits($msg, 32, 4) . "." . bits($msg, 36, 4);
	my $windchill = (bits($msg, 40, 1) ? "-" : "+") . bits($msg, 48, 4) . bits($msg, 52, 4);

	print STDERR "Wind: Bat: $battery Gust: ${gustspeed}m/s @ ${gustdir}deg Avg: ${avgspeed}m/s Chill: ${windchill}C\n" if $debug;
	return {"battery" => $battery,
		"gustdir" => $gustdir,
		"gustspeed" => $gustspeed,
		"avgspeed" => $avgspeed,
		"windchill" => $windchill,
		};
}
sub rrd_wind($) {
	my %hash = %{$_[0]};
	return ({"file" => "wind",
		 "template" => "vecN:vecE",
		 "value" => join( ':', cos( $hash{"gustdir"} / 180 * 3.141592654 ) * $hash{"gustspeed"}, 
		 			sin( $hash{"gustdir"} / 180 * 3.141592654 ) * $hash{"gustspeed"} ),
		 },
		 );
}

sub parse_rain($$) {
	my ($type, $msg) = @_;

	my $battery = bits($msg, 0, 4);
	#my undef = bits($msg, 4, 4);
	my $rate = bits($msg, 20, 4) . bits($msg, 8, 4) . bits($msg, 12, 4);
	my $tips = bits($msg, 16, 4);
	my $total = bits($msg, 32, 4) . bits($msg, 36, 4) . bits($msg, 24, 4) . bits($msg, 28, 4);
	my $yesterday = bits($msg, 48, 4) . bits($msg, 52, 4) . bits($msg, 40, 4) . bits($msg, 44, 4);
	my $since = bits($msg, 88, 4) . bits($msg, 92, 4) . "-" .
		bits($msg, 80, 4) . bits($msg, 84, 4) . "-" .
		bits($msg, 72, 4) . bits($msg, 76, 4) . " " .
		bits($msg, 64, 4) . bits($msg, 68, 4) . ":" .
		bits($msg, 56, 4) . bits($msg, 60, 4);

	print STDERR "Rain: Bat: $battery Rate: ${rate}mm/h Tips: $tips Total: ${total}mm since $since  Yesterday: ${yesterday}mm\n" if $debug;
	return {"battery" => $battery,
		"rate" => $rate,
		"tips" => $tips,
		"total" => $total,
		"yesterday" => $yesterday,
		};
}
sub rrd_rain($) {
	my %hash = %{$_[0]};
	return ({"file" => "rain",
		 "template" => "total",
		 "value" => join(':', $hash{"total"} ),
		 },
		);
}

sub parse_out($$) {
	my ($type, $msg) = @_;
	
	my $battery = bits($msg, 0, 4);
	#my undef = bits($msg, 4, 4);
	my $temp = (bits($msg, 16, 1) ? "-" : "+") . bits($msg, 20, 4) . bits($msg, 8, 4) . "." . bits($msg, 12, 4);
	my $humidity = bits($msg, 24, 4) . bits($msg, 28, 4);
	my $dewpoint = bits($msg, 32, 4) . bits($msg, 36, 4);

	print STDERR "Outside: Bat: $battery Temp: ${temp}C Humidity: $humidity% Dewpoint: ${dewpoint}C\n" if $debug;
	return {"battery" => $battery,
		"temp" => $temp,
		"humidity" => $humidity,
		"dewpoint" => $dewpoint
		};
}
sub rrd_out($) {
	my %hash = %{$_[0]};
	return ({"file" => "outside",
		 "template" => "temperature:humidity",
		 "value" => join(':', $hash{"temp"}, $hash{"humidity"} ),
		 },
		);
}

sub parse_in($$) {
	my ($type, $msg) = @_;

	my $battery = bits($msg, 0, 4);
	#my undef = bits($msg, 4, 4);
	my $temp = (bits($msg, 16, 1) ? "-" : "+") . bits($msg, 20, 4) . bits($msg, 8, 4) . "." . bits($msg, 12, 4);
	my $humidity = bits($msg, 24, 4) . bits($msg, 28, 4);
	my $dewpoint = bits($msg, 32, 4) . bits($msg, 36, 4);
	my $baro = bits($msg, 55, 1) * 0x100 + unpack("C", substr($msg, 5, 1)) + 600;
	my $forecast = unpack("B4", substr($msg, 6, 1)); # bits indicate iluminated symbols on the display:
	  if( $forecast eq "1100" ) { $forecast = "Sunny"; }
	  elsif( $forecast eq "0110" ) { $forecast = "Partly cloudy"; }
	  elsif( $forecast eq "0010" ) { $forecast = "Cloudy"; }
	  elsif( $forecast eq "0011" ) { $forecast = "Rain"; }
	#my undef = bits($msg,52,3);
	#my undef = bits($msg,56,8);
	#my undef = bits($msg,72,4);
	my $baro_offset = ( bits($msg, 76, 4) . bits($msg, 64, 4) . bits($msg, 68, 4) );
	my $baro_SL = $baro - 600 + $baro_offset;


	print STDERR "Inside: Bat: $battery Temp: ${temp}C Humidity: $humidity% Dewpoint: ${dewpoint}C ",
		"Baro: ${baro}mb (${baro_SL}mbSL) FC: $forecast\n" if $debug;
	return {"battery" => $battery,
		"temp" => $temp,
		"humidity" => $humidity,
		"dewpoint" => $dewpoint,
		"baro" => $baro,
		"baro sea level" => $baro_SL,
		"baro forecast" => $forecast,
		};
}
sub rrd_in($) {
	my %hash = %{$_[0]};
	return ({"file" => "inside",
		 "template" => "temperature:humidity",
		 "value" => join(':', $hash{"temp"}, $hash{"humidity"} ),
		 },
		{"file" => "baro",
		 "template" => "baro",
		 "value" => join(':', $hash{"baro"} ),
		 },
		);
}

sub parse_minute($$) {
	my ($type, $msg) = @_;
	
	my $minute = bits($msg, 0, 4) . bits($msg, 4, 4);

	print STDERR "Minute: $minute minutes past the hour\n" if $debug;
	return {"minute" => $minute};
}
sub rrd_minute($) {
	my %hash = %{$_[0]};
	return ();
}

sub parse_hour($$) {
	my ($type, $msg) = @_;
	
	my $battery = bits($msg, 0, 4);
	#my undef = bits($msg, 4, 4);
	my $hour = bits($msg, 8, 4) . bits($msg, 12, 4);
	my $day = bits($msg, 16, 4) . bits($msg, 20, 4);
	my $month = bits($msg, 24, 4) . bits($msg, 28, 4);
	my $year = bits($msg, 32, 4) . bits($msg, 36, 4);

	print STDERR "Hour: $year-$month-$day ${hour}h\n" if $debug;
	return {"battery" => $battery,
		"hour" => $hour,
		"day" => $day,
		"month" => $month,
		"year" => $year,
		};
}
sub rrd_hour($) {
	my %hash = %{$_[0]};
	return ();
}



my %type = (
	"\x00" => {"length" => 8,  "type" => "wind",   "parser" => \&parse_wind,   "rrd" => \&rrd_wind},
	"\x01" => {"length" => 13, "type" => "rain",   "parser" => \&parse_rain,   "rrd" => \&rrd_rain},
	"\x03" => {"length" => 6,  "type" => "out",    "parser" => \&parse_out,    "rrd" => \&rrd_out},
	"\x06" => {"length" => 11, "type" => "in",     "parser" => \&parse_in,     "rrd" => \&rrd_in},
	"\x0e" => {"length" => 2,  "type" => "minute", "parser" => \&parse_minute, "rrd" => \&rrd_minute},
	"\x0f" => {"length" => 6,  "type" => "hour",   "parser" => \&parse_hour,   "rrd" => \&rrd_minute},
	);


my ($rrdout, $rrdin);
my $rrdpid = open2($rrdout, $rrdin, '/usr/bin/rrdtool', '-');

sub INT_handler {
	print STDERR "Caught interrupt, quitting...\n";
	print $rrdin "quit\n";
	waitpid $rrdpid, 0;
	exit 0;
}
$SIG{'INT'} = 'INT_handler';
$SIG{'TERM'} = 'INT_handler';

my $state = "WAIT_FOR_SYNC";
while(1) {
	read STDIN, my $data, 1;
	if( $state eq "WAIT_FOR_SYNC" ) {
		if( $data eq "\xff" ) {
			$state = "SYNC";
		} else {
			print STDERR "OOS: ", unpack("H2", $data), "\n" if $debug;
		}
	} elsif( $state eq "SYNC" ) {
		if( $data eq "\xff" ) {
			$state = "TYPE";
		} else {
			$state = "WAIT_FOR_SYNC"; # we got here when we shouldn't have
			print STDERR "OOS: ", unpack("H2", $data), "\n" if $debug;
		}
	} elsif( $state eq "TYPE" ) {
		if( not defined $type{$data} ) {
			print STDERR "Unknown packet type ", unpack("H2", $data), "\n" if $debug;
		} else {
			read STDIN, my $msg, $type{$data}{"length"};
			if( verify_checksum($data . $msg) ) {
				my $parsed = $type{$data}{"parser"}($data, $msg);
				my @rrdcmds = $type{$data}{"rrd"}($parsed);
				foreach my $cmd (@rrdcmds) {
					my $t = "update \"$fileprefix." . $cmd->{file} . ".rrd\"";
					$t .= " --template " . $cmd->{template};
					$t .= " N:" . $cmd->{value};
					$t .= "\n";
					print STDERR "rrd< $t" if $debug > 1;
					print $rrdin $t;
					my $response = <$rrdout>;
					print STDERR "rrd> $response" if $debug > 1;
					print STDERR "Error updating rrd: $response\n" unless $response =~ m/^OK/;
				}
			} else {
				print STDERR "Corrupt packet, probably type ", unpack("H2", $data), "\n" if $debug;
			}
		}
		$state = "WAIT_FOR_SYNC";
	} else {
		die("Unknown state \"$state\"");
	}
}


