#!/usr/bin/perl -w
# -*- perl -*-
=head1 NAME

sensors_ - Wildcard-plugin to monitor information from temperature, voltage,
	   and fan speed sensors.

=head1 CONFIGURATION

The plugins needs the following components configured: 

	- i2c and lm_sensors modules installed and loaded.
	- sensors program installed and in path.
 Note:
 	- Sensor names are read from the output of the sensors program.
 	  Change them in /etc/sensors.conf if you don't like them.

  [sensors_*]
      env.sensors 	    - Override default sensors program path
      env.ignore_temp<n>    - Temperature <n> will not be plotted
      env.ignore_fan<n>     - Fan <n> will not be plotted
      env.ignore_volt<n>    - Voltage <n> will not be plotted
      env.fan_warn_percent  - Percentage over mininum for warning
      env.volt_warn_percent - Percentage over mininum/under maximum for warning

=head1 VERSION

$Id: sensors_.in 3431 2010-03-16 20:22:20Z feiner.tom $

=head1 AUTHOR

Unknown author

=head1 LICENSE

Unknown license

=head1 MAGIC MARKERS

 #%# family=manual
 #%# capabilities=autoconf suggest

=cut
use strict;

$ENV{'LANG'} = "C"; # Force parseable output from sensors.
$ENV{'LC_ALL'} = "C"; # Force parseable output from sensors.
my $SENSORS = $ENV{'sensors'} || 'sensors';

# Example outputs from sensors & matching regex parts:

# Fan output example from sensors: 
# --------------------------------
# Case Fan:   1268 RPM  (min = 3750 RPM, div = 8)  ALARM
# CPU Fan:       0 RPM  (min = 1171 RPM, div = 128)  ALARM
# Aux Fan:       0 RPM  (min =  753 RPM, div = 128)  ALARM
# fan4:       3375 RPM  (min =  774 RPM, div = 8)
# fan5:          0 RPM  (min = 1054 RPM, div = 128)  ALARM
# 
# ^^^^        ^^^^             ^^^^      
#  $1          $2               $3
# 
# --------------------------------
# 
# Temperature output example from sensors:
# * Note that the iso-8859-1 degree character is not printed, as we are running
#   with LC_ALL=C & LANG=C.
# ---------------------------------------
# Sys Temp:    +41.0 C  (high = -128.0 C, hyst = +24.0 C)  ALARM  sensor = thermistor
# CPU Temp:    +40.5 C  (high = +80.0 C, hyst = +75.0 C)  sensor = thermistor
# AUX Temp:    +39.0 C  (high = +80.0 C, hyst = +75.0 C)  sensor = thermistor
# 
# ^^^^^^^^      ^^               ^^              ^^
#   $1          $2               $3              $4
# 
# ---------------------------------------
# 
# Voltage output example from sensors:
# ------------------------------------
# 
# VCore:       +1.09 V  (min =  +0.00 V, max =  +1.74 V)
# in1:        +12.14 V  (min = +10.51 V, max =  +2.38 V)   ALARM
# 
# ^^^         ^^^              ^^^              ^^
# $1          $2               $3               $4
# 
# 
# ------------------------------------


my %config = (
    fan => {
        regex => qr/
            ^ # String must start with:
            (\S[^:]*)  # Match any non-whitespace char, except ':' 
                       # (Match label as \$1)
            \s*        # Zero or more spaces followed by 
            :          # : characheter
            \s*        # Zero or more spaces followed by
            \+?        # Zero or one '+' char. Note: This might not be needed  
                       # as FAN speeds don't have + signs in front of them.
                       # This can be probably be removed as the 
                       # sensors program never prints a + char in front of
                       # RPM values. Verified in lm-sensors-3-3.1.2
            (\d+)      # Match one or more digits (Match current value as \$2)
            \s         # Exactly one space followed by 
            RPM.*?     # RPM string, and then any chars
            (\d+)      # Match one or more digits (Match minimum value as \$3)
            \s         # Exactly one space followed by
            RPM        # RPM string
                /xms,
            title => 'Fans',
            vtitle => 'RPM',
            print_threshold => \&fan_threshold,
            graph_args => '--base 1000 -l 0'
      },

    temp => {
        regex => qr/
            ^ # String must start with:
            (\S[^:]*)  # Match any non-whitespace char, except ':' 
                       # (Match label as \$1)
            \s*        # Zero or more spaces followed by 
            :          # ':' characheter
            \s*        # Zero or more spaces followed by
            \+?        # Zero or one '+' char followed by

            (-?        # Match zero or one '-' char  
            \d+        # Match one or more digits 
                       # (Match current value as \$2) followed by

            (?:\.\d+)?)# Zero or one match of '.' char followd by one or more
                       # digits. '?:' means it doesn't spit out the field.
            [\s]      # Match degree char or space char
            C          # Match 'C' char

            (?:        # >>>>Match the following statement zero or one time. 
                       # '?:' means it doesn't spit out the field.
            \s+        # One or more space followed by
            \(         # '(' char
            (?:high|limit) # 'high' or 'limit' string.  
                           # '?:' means it doesn't spit out the field.
            \s*=\s*    # Match zero or more spaces and then '=' char and then
                       # zero or more spaces, followed by
            \+?        # Zero or one '+' char
            (\d+       # Match one or more digits 
                       # (Match high value as \$3) followed by  
            
            (?:\.\d+)?)# Zero or one match of '.' char followd by one or more
                       # digits. '?:' means it doesn't spit out the field.
            [\s]      # Match degree char or space char
            C,\s*      # 'C,' string followd by zero or more spaces
            hyst       # 'hyst' string followed by
            (?:eresis)?# zero or one 'eresis' string. 
                       # '?:' means it doesn't spit out the field.
            \s*=\s*    # Match zero or more spaces and then '=' char and then 
                       # zero or more spaces, followed by
            \+?        # Zero or one '+' char
            (\d+       # Match one or more digits 
                       # (Match hyst value as \$4) followed by  
            (?:\.\d+)?)# Zero or one match of '.' char followd by one or more
                       # digits. '?:' means it doesn't spit out the field.
            [\s]C     # Match degree char or space char, and then 'C' char
            \)         # ')' char
            )?         # >>>>end of statement
        /xms,
    title => 'Temperatures',
    vtitle => 'Celsius',
    print_threshold => \&temp_threshold,
    graph_args => '--base 1000 -l 0'
    },

    volt => {
        regex => qr/
            ^           # String must start with:
            (\s?\S[^:]*)# Match any non-whitespace char, except ':'
                        # but allow an optional whitespace at the begining.
                        # This is needed as voltages are sometimes printed with
                        # one space in front of them.
                        # (match the label as \$1)

            \s*:\s*     # Zero or more spaces followed by ':' char and 
                        # the zero or more spaces followed by
            \+?         # Zero or one '+' char followed by
            (-?         # Match zero or one '-' char
            \d+         # Match one or more digits 
                        # (match the current voltage as \$2) followed by
            (?:\.\d+)?) # Zero or one match of '.' char followd by one or more digits. 
                        # '?:' means it doesn't spit out the field.
            \sV         # one space, 'V' char

            (?:         # >>>>Match the following statement. 
                        # '?:' means it doesn't spit out the field.
            \s+         # One or more space followed by
            \(          # '(' char
            min         # Match min string
            \s*=\s*     # Match zero or more spaces and then '=' char and 
                        # then zero or more spaces, followed by
            \+?         # Zero or one '+' char
            (-?         # Match zero or one '-' char
            \d+         # Match one or more digits 
                        # (Match the minimum value as \$3) followed by
            (?:\.\d+)?) # Zero or one match of '.' char followd by one or more
                        # digits. '?:' means it doesn't spit out the field.
            \sV,\s*     # One space char, 'V,' string followd by zero or 
                        # more spaces

            max         # Match 'max' string
            \s*=\s*     # Match zero or more spaces and then '=' char and 
                        # then zero or more spaces, followed by
            \+?         # Zero or one '+' char
            (-?         # Match zero or one '-' char
            \d+         # Match one or more digits  
                        # (Match the max value as \$4) followed by
            (?:\.\d+)?) # Zero or one match of '.' char followd by one or more digits. 
                        # '?:' means passive group (does not match)
            \sV         # one space, 'V' char
            \)          # ')' char
            )           # >>>>end of statement

        /xms,

    title => 'Voltages',
    vtitle => 'Volt',
    print_threshold => \&volt_threshold,
    graph_args => '--base 1000 --logarithmic'
        },
);

if ( defined $ARGV[0] and $ARGV[0] eq 'autoconf' ) {
  # Now see if "sensors" can run
  my $text = `$SENSORS 2>/dev/null`;
  if ($?) {
    if ($? == -1) {
      print "no (program $SENSORS not found)\n";
    } else {
      print "no (program $SENSORS died)\n";
    }
    exit 0;
  }

  unless ($text =~ /[ ]C/) {
    print "no (no temperature readings)\n";
    exit 0;
  }

  print "yes\n";
  exit 0;
}

if (defined $ARGV[0] and $ARGV[0] eq 'suggest') {
  my $text = `$SENSORS 2>/dev/null`;
  foreach my $func (keys %config) {
    print $func, "\n" if $text =~ $config{$func}->{regex};
  }
  exit;
}

$0 =~ /sensors_(.+)*$/;
my $func = $1;
exit 2 unless defined $func;

if ( defined $ARGV[0] and $ARGV[0] eq 'config' ) {
  print "graph_title $config{$func}->{title}\n";
  print "graph_vtitle $config{$func}->{vtitle}\n";
  print "graph_args $config{$func}->{graph_args}\n";
  print "graph_category sensors\n";
  my $text = `$SENSORS`;
  my $sensor = 1;
  while ($text =~ /$config{$func}->{regex}/g) {
    my ($label, undef, $max, $min) = ($1, $2, $3, $4);
    print "$func$sensor.label $label\n";
    $config{$func}->{print_threshold}->($func.$sensor, $3, $4);
    print "$func$sensor.graph no\n" if exists $ENV{"ignore_$func$sensor"};
    $sensor++;
  }
  exit 0;
}

my $text = `$SENSORS`;
my $sensor = 1;
while ($text =~ /$config{$func}->{regex}/g) {
  print "$func$sensor.value $2\n";
  $sensor++;
}

sub fan_threshold {
  my $name = shift;
  my $min = shift;

  my $warn_percent = exists $ENV{fan_warn_percent} ? $ENV{fan_warn_percent} : 5;

  return unless defined $min;

  printf "$name.warning %d:\n", $min * (100 + $warn_percent) / 100;
  printf "$name.critical %d:\n", $min;
}

sub temp_threshold {
  my $name = shift;
  my $max = shift;
  my $min = shift;

  printf "$name.warning $min\n" if $min;
  printf "$name.critical $max\n" if $max;
}

sub volt_threshold {
  my $name = shift;
  my $min = shift;
  my $max = shift;
  my $warn_percent = exists $ENV{volt_warn_percent} ? $ENV{volt_warn_percent} : 20;

  return unless defined ($min && $max);

  my $diff = $max - $min;
  my $dist = $diff * $warn_percent / 100;
  printf "$name.warning %.2f:%.2f\n", $min + $dist, $max - $dist;
  printf "$name.critical $min:$max\n";
}

# vim:syntax=perl
