#!/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 = '2005.05.27.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 = '/www/weather'; # Make sure this and progname are accurate or # HUP signals won't work, plus other problems. my $admin_email = 'someone@yourdomain.tld'; # 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 = ''; # Leave blank to disable WU updating. 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 | /usr/bin/grep $progname |"; while () { $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 $debug = 0; 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 () { 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; } } 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}\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 $tmp = $data[$i++]; my $tmp2 = $data[$i++]; $info{windgustdir} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp)+0; my $tmp = $data[$i++]; $info{windgustspeed} = sprintf("%02x%02x",$tmp,$tmp2 & 0xF0)/100; my $tmp = $data[$i++]; my $tmp2 = $data[$i++]; $info{windavgspeed} = sprintf("%02x%02x",$tmp2 & 0x0F,$tmp)/10; my $tmp = $data[$i++]; $info{windchill} = $tmp; $info{windchill} = '-'.$tmp if ($tmp2 & 0x80); &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++]+1850; &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{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++]; 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}\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 = ; 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 = ; 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"; }