The following code is configured to check the Garmin website for updates to the MapSource software package, and to the GPSMap76 firmware. Though that is a rather specific use, I'm posting it here as it may be used as a starting point for keeping up to date on new versions of software from other websites as well. Without too much work, it could be made to find updates for Netgear routers and NIC's, for example.

The script is made to run every few days as a Windows Scheduler item, or in a cron job (some minor modification necessary), or from the command line. No flocking is done, so care should be taken that the script won't be run multiple times at the same instant. ...this shouldn't be an issue anyway.

The script (as currently configured) grabs the software downloads page from Garmin's website using LWP::Simple. It then uses HTML::TableExtract to figure out which table on that page contains the entries for the MapSource product, and the GPSMap76 product. The same module pulls in the relevant information for those entries.

Next, the script reads in the data file from the local hard drive to ascertain what versions of the two packages were found on last run. This run's data is compared against the previous run's data using Sort::Versions.

If an increase of version number is detected for either package, the script keeps track of which package(s) have updated downloads available. This information is dumped to STDOUT, and also (optionally) an email message may be sent as notification of the available updates. The message is sent using MIME::Lite.

Finally, if a new version was detected, the updated version numbers are re-saved to the data file. It should be noted that on the very first run it is expected that the datafile doesn't already exist. That's fine. It will be created with first-run data. Also, on first run, since there is no datafile to compare to, the script assumes that the version numbers pulled from the website are updates. I left this in the logic so that on first-run you'll get a sample of what you should see any time one of the packages gets updated.

Since the script doesn't really have any redundant or repetative code, and for the most part just glues together with some logic the functionality of a bunch of modules, I chose not to use any subroutines. However, I did use lexical blocks to limit scoping of lexical variables as much as was practical, and the script really does flow along a simple path from start to finish. It should be pretty legible, and subroutines wouldn't really have improved readability or flow any, IMHO.

To configure the script to search for other GPS's on Garmin's site, just review the table in question to get the correct spelling of the GPS's name, and then change the initialization value of @devices. The contents of @devices gets joined together in '|' alternation as one big regexp.

The rest of the configuration should be pretty simple. I left the configuration variables at the top of the script. If you wish to customize this to work for different websites, look at the HTML::TableExtract ( headers=> stuff to define what headers will signal the table you're trying to pull data from. If you need to pull the data from a webpage that doesn't use tables, you'll have to use a different parser like HTML::TokeParser. But this should be a good startingpoint.

After that long writeup, here's the code...

use strict; use warnings; use LWP::Simple; use HTML::TableExtract; use Sort::Versions; use MIME::Lite; # A few configuration variables... my $datafile = 'gpsdata.dat'; my $website = 'http://www.garmin.com/support/download.jsp'; my $mailfrom = 'your@return.address'; my $email_notify = 'Yes'; my $mailto = 'notification@email.address'; my $mailhost = 'smtp.host.net'; my @devices = ( 'GPSMAP 76', 'MapSource' ); my %found_device; { # Pull in the webpage... my $raw_html = get( $website ); # For testing, save the webpage as 'garmin.htm', comment out the # preceeding line, and uncomment the following six lines. This # will test against the saved version rather than hitting # the remote site. # my $raw_html = do { # open my $in, '<', 'garmin.htm' # or die "Can't open infile: $!\n"; # local $/ = undef; # <$in>; # }; # Define which tables to search. my $te = new HTML::TableExtract( headers => [ 'Product\s+Name', 'Software\s+Version', 'Compatible\s+with\s+Versions', 'Date' ] ); $te->parse($raw_html); # Find and keep track of the applicable entries. my $find_devices = join '|', @devices; my $re_devices = qr/$find_devices/; foreach my $ts ( $te->table_states ) { foreach my $row ( $ts->rows ) { s/^\s+// foreach @{$row}; s/\s+$// foreach @{$row}; $found_device{ $row->[0] } = [ @$row ] if $row->[0] =~ /^(?:$re_devices)$/i; } } } # Check for and pull in previous-run data. my %prev_device = (); if ( -e $datafile ) { open my $dat, '<', $datafile or die "Couldn't open input file: $!\n"; while ( my $line = <$dat> ) { chomp $line; my @row = split /\s*\|\s*/, $line; $prev_device{ $row[0] } = [ @row ]; } close $dat; } # Compare site data with stored data. my @changed; foreach my $key ( keys %found_device ) { push @changed, $key if ( ( not defined $prev_device{$key} ) or versioncmp( $prev_device{$key}[1], $found_device{$key}[1] ) == -1 ); } # We're done if there are no updates available. unless ( @changed ) { print "No updates available at $website.\n"; exit; } # If updates, print results and, send an email message. my $out_text = "Download from $website:\n"; foreach ( @changed ) { $out_text .= "Upgrade available for $_: " . " version $found_device{$_}[1], " . "$found_device{$_}[3]\n"; } print $out_text; # Email stuff. if ( $email_notify =~ /^y/i ) { my $msg = MIME::Lite->new( 'From' => $mailfrom, 'To' => $mailto, 'Subject' => "[Upgrade] Updates available for " . do { local $" = ', '; "@changed"; } . ".", 'Data' => $out_text ); # This line is for us Windows folks. MIME::Lite->send( 'smtp', $mailhost, 'Timeout' => 60 ); $msg->send(); } # Save updates to data file. open my $fh, '>', $datafile or die "Can't write update to $datafile: $!\n"; foreach my $key ( keys %found_device ) { local $" = '|'; print $fh "@{$found_device{$key}}\n"; } close $fh or die "Can't close $datafile after update: $!\n";

Comments welcome, of course. I'm always interested in learning.

Update: One final thought. This script contains commented-out code that would make it very easy for the script to operate on a saved version of a webpage, rather than grabbing it with LWP::Simple. When testing, I encourage you to save the page you're testing against, and use the portion of commented-out code to read in that file rather than hit Garmin's site. Play nice so nobody gets upset. :)


Dave