#!/usr/bin/perl -w ## # Fetch the current temperture from the cambridge computer lab # sensors. # # This plugin is able to run as either a nagios plugin, a munin # plugin, or standalone. # # For munin, place the script in /usr/share/munin/plugins/cambridge-dtg_ # and run munin-node-configure as usual. # # For nagios, place the script in a suitable location (eg # /etc/mrtg/cambridge-dtg.pl) and invoke it with : # -mrtg -reading . # # # Munin configuration details: # #%# family=auto #%# capabilities=autoconf suggest # eval { use LWP::Simple qw/get/; use Getopt::Long; use Data::Dumper; }; if ($@) { if ($ARGV[1] eq 'autoconf') { die "no (Cannot load modules)\n"; } } my $munin = 0; my $mrtg = 0; my $debug = 0; my $cache = "/tmp/camb-dtg-$>.txt"; my $cacheage = 1/24/60; # 1 minute my $reading = 'Temperature'; GetOptions('reading=s' => \$reading, 'debug' => \$debug, 'mrtg' => \$mrtg, 'munin' => \$munin, ); # Override to be munin if the variable indicates we're running as it. if (defined $ENV{'MUNIN_VERSION'}) { $munin = 1; } my $url = "http://www.cl.cam.ac.uk/research/dtg/weather/current-obs.txt"; my $page = ''; # When invoked by munin we may be called multiple times to get our readings. # We don't want to be repeatedly talking to the remote server so we'll cache # the results in a local file ($cache). # FIXME: Consider race conditions and lock files? if (!-f $cache || -M $cache > $cacheage) { $page = get($url) || ''; open(my $fh, '>', $cache); if (defined $fh) { print $fh $page; close($fh); } } else { if (open(my $fh, '<', $cache)) { sysread($fh, $page, -s $cache); close($fh); } } # The text produced is something like: # # Cambridge Computer Laboratory Rooftop Weather at 10:36 AM on 19 Sep 11: # # Temperature: 15.5 C # Pressure: 1011 mBar # Humidity: 68 % # Dewpoint: 9.6 C # Wind: 6 knots from the SW # Sunshine: 2.7 hours since midnight # Rainfall: 0.0 mm since midnight # # Summary: sunny, mild, windy ## # Decode the values into a hash keyed by the label. # The values will be the raw strings from the above text. my %readings = ($page =~ /^([\w]*?): +(.*)$/mg); ## # A hash of details for each of the fields returned from the # textual results. # Each function is passed the value from the file and # returns an array of: # Value (may be undef if not parseable) # Units # Label # Description # [ Low, High ] (may be absent) my %conversions = ( 'Temperature' => sub { my ($value) = @_; return (convert_to_celcius($value), 'C', 'Temperature', 'Temperature in degrees Celcius', [ -30, 40 ], # Rounded to nearest 10 # see: # http://www.metoffice.gov.uk/climate/uk/extremes/#temperature ); }, 'Dewpoint' => sub { my ($value) = @_; return (convert_to_celcius($value), 'C', 'Dewpoint', 'Dewpoint in degrees Celcius', ); }, 'Pressure' => sub { my ($value) = @_; ($value) = ($value =~ /([\d\.]+) mBar/); # Note that hPa and millibars are the same when used for # air pressure. return ($value, 'mBar', 'Pressure', 'Pressure in millibars', [ 870, 1085 ] # May be a little extreme; see: # http://en.wikipedia.org/wiki/Atmospheric_pressure#Atmospheric_pressure_records ); }, 'Humidity' => sub { my ($value) = @_; ($value) = ($value =~ /([\d\.]+) *\%/); return ($value, '%', 'Humidity', 'Humidity of air as a percentage', [ 0, 100 ]); }, 'Wind' => sub { my ($value) = @_; ($value) = ($value =~ /([\d\.]+) knots from the ([NWSE]+)/); # Ignore the direction for now return ($value, 'knots', 'Wind speed', 'Wind speed in knots', [ 0, 130 ], # founded to next 10 # http://www.metoffice.gov.uk/climate/uk/extremes/#gust_speed # for extremes, see: # http://en.wikipedia.org/wiki/Wind_speed#Highest_speed ); }, 'Sunshine' => sub { my ($value) = @_; ($value) = ($value =~ /([\d\.]+) hours since midnight/); return ($value, 'hours', 'Sunshine', 'Hours of sunshine since midnight', [ 0, 24 ], # If we get 24 hours of sunshine, something is very, # very wrong with the world. ); }, 'Rainfall' => sub { my ($value) = @_; ($value) = ($value =~ /([\d\.]+) mm since midnight/); return ($value, 'mm', 'Rainfall', 'Rainfall in mm since midnight', [ 0, 320 ], # Rounded to next 10 # http://www.metoffice.gov.uk/climate/uk/extremes/#rainfall ); }, ); # Parse all the data we received using the conversion functions my %parsed; for my $key (keys %readings) { if (defined $conversions{$key}) { # Cal the conversion function to get our values my $str = $readings{$key}; my $func = $conversions{$key}; my ($value, $units, $label, $desc, $limits) = & $func ( $str ); $parsed{$key} = { 'key' => $key, 'value' => $value, 'units' => $units, 'label' => $label, 'desc' => $desc, 'limits' => $limits }; } } # Mapping for munin - the values which will go together. # Keyed by the munin label that will return them. # Each value is an array of the readings to use as a # comma separated list, followed by the title of the # graph (or undef if the same as the readings). my %muninmap = ( 'temperature' => [ 'Temperature,Dewpoint', 'Temperatures' ], 'rainfall' => [ 'Rainfall' ], 'sunshine' => [ 'Sunshine' ], 'wind' => [ 'Wind' ], 'humidity' => [ 'Humidity' ], 'pressure' => [ 'Pressure' ], ); if ($mrtg) { # For MRTG we can only return up to 2 values, from the command line # arguments. my @vals = map { $parsed{$_} } split /,/, $reading; # Make sure there's always a 0 in the second field if none # was supplied. if (@vals == 1) { push @vals, { 'value' => 0 }; } for (0..1) { my $value = $vals[$_]; if (defined $value) { $value = $value->{'value'}; print "$value\n"; } else { print "UNKNOWN\n"; } } } elsif ($munin) { # For munin we can return any number of values, but only through the # munin map of graph-types-to-readings. my $arg = shift || ''; my $title; if ($0 =~ /_(\w+)$/) { ($reading, $title) = @{ $muninmap{$1} }; } else { ($reading, $title) = @{ $muninmap{'temperature'} }; } if (!defined $title) { $title = $reading; } # Title should always have the location appended to be clear $title .= " at the Cambridge DTG"; if ($arg eq 'autoconf') { # Must return 'yes' or 'no' depending on whether it can run if (keys %parsed > 0 && defined $reading) { print "yes\n"; } else { print "no\n"; } } elsif ($arg eq 'suggest') { # Must suggest the names of the graphs it can generate if (keys %parsed > 0 && defined $reading) { print map { "$_\n" } keys %muninmap; } else { # Do nothing if we couldn't reach the server } } elsif ($arg eq 'fetch' || $arg eq '') { # Obtain the readings for this graph my @list = split /,/, $reading; for my $name (@list) { my $value = $parsed{$name}; $value = {'value' => 'U'} if (!defined $value); print "$name.value $value->{'value'}\n"; } } elsif ($arg eq 'config') { # Configuration details are a little more complex. print "graph_title $title\n"; # It's not really 'sensors' because it's not monitoring # things in the location, so I place it separately. # This is probably a matter of local choice. print "graph_category Environment\n"; print "graph_info This graph shows environmental " . "information collected by the Cambridge " . "University Digital Technology Group\n"; my @list = split /,/, $reading; if (@list > 0 && defined $parsed{$list[0]}) { my $first = $parsed{$list[0]}; my $units = $first->{'units'}; $units = " ($units)" if ($units ne ''); print "graph_vlabel $first->{'label'}$units\n"; } # Now details about each field for my $name (@list) { my $value = $parsed{$name}; next if (!defined $value); print "$name.label $value->{'label'}\n"; print "$name.info $value->{'desc'}\n"; if (defined $value->{'limits'}) { print "$name.min $value->{'limits'}->[0]\n"; print "$name.max $value->{'limits'}->[1]\n"; } } } else { print "unrecognised operation: $arg\n"; } } else { print "Textual results:\n"; for my $key (sort keys %parsed) { my $value = $parsed{$key}; printf "%12s : %5s %-6s (%s)\n", $value->{'label'}, $value->{'value'} // 'N/A', $value->{'units'}, $value->{'desc'}; } } ## # Convert a temperature to celcius, allowing for units # of 'C', 'K' and 'F'. # # @param[in] $str String to convert # # @return Value in celcius, or undef if cannot parse. sub convert_to_celcius { my ($str) = @_; my $value = undef; if ($str =~ /([\d\.]+) *([CKF]?)/) { $value = $1; # Convert the temperature to celsius if (!defined $2 || $2 eq 'C') { # No conversion necessary } elsif ($2 eq 'K') { $value += 273.15; } elsif ($2 eq 'F') { $value = ($value - 32) * 5/9; } } return $value; }