#!/usr/bin/perl -w # +++ consider -T for taint-checks # +++ consider Perl::Critic # +++ consider Smart::Comments # +++ podchecker # +++ add deliberate random delay before post use 5.008; use strict; use warnings; # use diagnostics; use Getopt::Long qw(:config gnu_getopt); use Pod::Usage; use LWP::Simple; our $VERSION = 1.03; my (%opt, @opt); =head1 NAME Acurite2wunder.pl - send AcuRite rtl_433 output to wunderground =head1 SYNOPSIS Acurite2wunder.pl [ --help | --man ] rtl_433 -q -G -T 40 2>&1 | Acurite2wunder.pl [ --url http://... ] =head1 DESCRIPTION B will watch rtl_433 output for AcuRite packets. When it finds any, it will send them to wunderground.com (or via NoseyWX to CWOP/APRS-IS as well) =head1 OPTIONS =over =item B<--help> Print brief help and exits. =item B<--man> Prints the manual page and exits. =cut opt('help|?' => sub { pod2usage(-verbose => 1); }); opt('man' => sub { pod2usage(-verbose => 2); }); =item B<--version> Print some version information =item B<-v>, B<--verbose> Make the program more verbose. Can also use B<--verbose>=I<2> or B<-vvv> etc for more debugging spew =item B<-q>, B<--quiet> Same as B<--verbose>=I<0> =cut opt('version' => sub { die "This is $0 version $VERSION.\n"; }); opt('verbose|v:+' => 1); opt('quiet|q' => sub {$opt{verbose} = 0}); =item B<-n>, B<--dryrun>, B<--dry-run> Don't ACTUALLY send to wundergrounnd, just show what WOULD be sent. =cut opt('dryrun|dry-run|n!'); =item B<--url>=I Sends to the specified URL. Default http://weatherstation.wunderground.com/weatherstation/updateweatherstation.php?... =cut opt('url=s' => "http://weatherstation.wunderground.com/weatherstation/" . "updateweatherstation.php?action=updateraw&softwaretype=NoseyAcurite"); =item B<-i> I, B<--id>=I =item B<-p> I<12345678>, B<--password>=I<12345678> Specify your wunderground ID and PASSWORD from when you registered - MANDATORY (and no, that is NOT my real password) =cut opt('id|i=s'); opt('password|p=s'); =item B<-u>, B<--utc> Signals that timestamps in the logfile are in UTC, EG: "TZ=UTC rtl_433 -G". WUnderground likes "dateutc", otherwise we have to say "dateutc=now" :-/ =cut opt('utc|u!'); ###################################################################### # +++ copy every OPTION into SYNOPSIS sub opt { my ($name, $def) = @_; push @opt, lc($name); if (defined ($def) && $name =~ /^(\w+|<>)/) { $opt{lc $1} = $def } } # Do stuff: GetOptions(\%opt, @opt) or pod2usage(); # print usage if opt error pod2usage("No --ID provided") unless $opt{id}; pod2usage("No --PASSWORD provided") unless $opt{password}; my $tz = $opt{utc} ? 'dateutc' : 'datelocal'; my $on; my %dat = ( dateutc => 'now' ); while (<>) { chomp; if (/^{("time" .*)}$/) { # JSON-style $dat{valid} = 1; my $obj = "$1,"; while ($obj =~ s/^"(.*?)" *: (.*?), *//) { my ($k, $v) = ($1, $2); $v =~ s/^"(.*)"$/$1/; $dat{$k} = $v; if ($v =~ /^(\d+\.?\d*)/) { $dat{"$k#tot"} += $1; $dat{"$k#avg"} = $dat{"$k#tot"} / ++$dat{"$k#n"}; $dat{"$k#max"} //= $1; $dat{"$k#max"} = $1 if $1 > $dat{"$k#max"}; $dat{"$k#min"} //= $1; $dat{"$k#min"} = $1 if $1 < $dat{"$k#min"}; } } } elsif (/^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d).*\sAcurite .* Total rain .*: (.*)/) { $dat{$tz} = $1; $dat{rainfall_total} = $2; # Old-style Special Snowflake } elsif (/^(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d).*\sAcurite/) { $dat{$tz} = $1; $on = 1; # Old-style Acurite data begins } elsif (/^time\s*:\s*(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)/) { $dat{$tz} = $1; # New-style timestamp } elsif (/^model\s*:\s*Acurite.5n1/) { $on = 1; # New-style Acurite data begins } elsif (/^Rainfall Accumulation: (.+?) in/ && $on) { $dat{rainfall_total} = $1; # New-style Special Snowflake } elsif ( /^$/ || /^rtl_433 version / || /^Trying conf file at / || /^quiet option .* is default and deprecated/ || /^\s+Consider using "-M newmodel" to transition to new model/ || /^\s+A table of changes and discussion/ || /^Registered \d+ out of \d+ device decoding protocols/ || /^Detached kernel driver/ || /^Reattached kernel driver/ || /^Found .* tuner$/ || /^Exact sample rate/ || /^Sample rate set/ || /^Bit detection level set/ || /^Tuner gain set/ || /^Tuned to/ || /^usb_claim_interface error/ || /^Failed to open rtlsdr device/ || /^Time expired, exiting/ || /^WARNING: async read failed/ || /^User cancel, exiting/ || /^_ _ _ _ _ _ _/ ) { # Ignore these lines, BUT a new rtl_443 was run, so: $on = 0; } elsif (/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/) { print "# $_\n" if $opt{verbose} > 1; $on = 0; # some other sensor } elsif (/^\s*(\S+)\s*:\s+(.*)/ && $on) { $dat{valid} = 1; my ($k, $v) = ($1, $2); $dat{$k} = $v; if ($v =~ /^(\d+\.?\d*)/) { $dat{"$k#tot"} += $1; $dat{"$k#avg"} = $dat{"$k#tot"} / ++$dat{"$k#n"}; $dat{"$k#max"} //= $1; $dat{"$k#max"} = $1 if $1 > $dat{"$k#max"}; $dat{"$k#min"} //= $1; $dat{"$k#min"} = $1 if $1 < $dat{"$k#min"}; } } else { print "# $_\n" if $opt{verbose} > 1; } } pod2usage("No valid data recieved") unless $dat{valid}; my $url = "$opt{url}&ID=$opt{id}&PASSWORD=$opt{password}"; sub wx { my ($in, $out, $munge) = @_; my $val = $dat{$in}; return unless defined $val; if ( ref($munge) eq 'CODE' ) { $val = $munge->($val); } elsif ( ref($munge) eq 'Regexp') { return unless $val =~ /$munge/; $val = $1; } elsif ($munge) { $val *= $munge; } return unless defined $val; $val =~ s/([^-A-Za-z0-9_.:])/ $1 eq ' ' ? '+' : sprintf("%%%0x", ord $1) /ge; $url .= '&' . ($out||$in) . '=' . $val; return 1; } # battery = OK? # channel = C? wx( 'dateutc' ); # time || datelocal = 2017-10-07 17:16:27 -> dateutc? wx( 'humidity' ); # message_type = (49|56)? # NOT dailyrainin, which is "rain inches so far today in local time"? :-/ # totrainin is not wunderground-compatible, but wunder2aprs.cgi # translates to dailyrainin for me. wx( rainfall_total => 'totrainin') || wx( rain_in => 'totrainin') || wx( raincounter_raw => 'totrainin', 0.01); # rainfall_accumulation = 0.00 in # - almost ALWAYS 0.00 in, occasionally NEGATIVE so essentially useless # sensor_id = 0x68F # sequence_num = 1 wx( temperature_F => 'tempf' ) || wx( temperature => 'tempf', qr/^([0-9.]+) *F$/ ); # valid = 1 wx( wind_dir_deg => 'winddir') || wx( wind_dir => 'winddir', sub { return { N => 0, NNE => 23, NE => 45, ENE => 68, E => 90, ESE => 113, SE => 135, SSE => 158, S => 180, SSW => 203, SW => 225, WSW => 248, W => 270, WNW => 293, NW => 315, NNW => 338, } -> {shift()}} ); wx( 'wind_speed#avg' => 'windspeedmph' ) || wx( 'wind_avg_km_h#avg' => 'windspeedmph', 0.62137119) || wx( wind_speed => 'windspeedmph', qr/^([0-9.]+) *mph/ ); wx( 'wind_speed#max' => 'windgustmph' ) || wx( 'wind_avg_km_h#max' => 'windgustmph', 0.62137119); if ($opt{verbose}) { $dat{URL}=$url; for (sort keys %dat) { print "$_ = $dat{$_}\n"; } } if ($opt{dryrun}) { print "curl '$url'\n"; exit 0; } alarm 10; print get($url) || "NO-WX", "\n"; ###################################################################### __END__ =back =head1 RETURN VALUE returns 0 on success, 1 for warnings, 2 for errors =head1 ERRORS =head1 DIAGNOSTICS =head1 EXAMPLES rtl_433 -q -G -T 40 2>&1 | Acurite2wunder.pl =head1 ENVIRONMENT =head1 FILES =head1 BUGS =head1 NOTES =head1 SEE ALSO LEnoseynick.netE> =head1 AUTHOR "Nosey" Nick Waterman of Nilex Eperl@noseynick.orgE L<< http:EEnoseynick.orgE >> =head1 COPYRIGHT (C) Copyright 2017 Nilex - All wrongs righted, all rights reserved. =cut