#!@@PERL@@ -w
# -*- perl -*-
=head1 NAME

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

=head1 CONFIGURATION

The possible wildcard values are the follwing: fan, temp, volt. So you
would create symlinks to this plugin called sensors_fan, sensors_temp,
and sensors_volt.

The plugins needs the following components configured:

=over

=item i2c and lm_sensors modules installed and loaded.

=item sensors program installed and in path.

=back

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 AUTHOR

Unknown author

=head1 LICENSE

GPLv2

=head1 MAGIC MARKERS

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

=cut

use strict;

$ENV{'LANG'} = "C"; # Force parsable output from sensors.
$ENV{'LC_ALL'} = "C"; # Force parsable 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 degree character is replaced by a space, as we are running
#   with LC_ALL=C and 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:
            ([^:\n]*)  # Match any non-whitespace char, except ':' and new line
                       # (Match label as \$1)
            \s*        # Zero or more spaces followed by
            :          # : character
            \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
            (?:        # a optional group, which might contain the minimum value:
              RPM.*?   # RPM string, and then any chars (but no newlines)
              (\d+)    # Match one or more digits (Match minimum value as \$3)
              \s       # Exactly one space followed by
            )?         # end of optional group
            RPM        # RPM string
                /xm,   # Use extended regular expressions and treat string as multiple lines.
            title => 'Fans',
            vlabel => 'RPM',
            print_threshold => \&fan_threshold,
            graph_args => '--base 1000 -l 0'
      },

    temp => {
        regex => qr/
            ^          # String must start with:
            ([^:\n]*)  # Match any non-whitespace char, except ':' and new line
                       # (Match label as \$1)
            \s*        # Zero or more spaces followed by
            :          # ':' character
            \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 followed by one or more
                       # digits. '?:' means it is not a numbered capture group.
            [°\s]      # Match degree char or space char
            C          # Match 'C' char

            (?:        # >>>>Match the following statement zero or one time.
                       # '?:' means it is not a numbered capture group.
            \s+        # One or more space followed by
            \(         # '(' char
            (?:high|limit|crit) # 'high' or 'limit' or 'crit' string.
                       # '?:' means it is not a numbered capture group.
            \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 followed by one or more
                       # digits. '?:' means it is not a numbered capture group.
            [°\s]      # Match degree char or space char
            C,?\s*     # 'C' followed by optional comma and zero or more spaces
            (?:        # >>>Match the following non-capture group zero or more times
            (?:crit|hyst(?:eresis)?) # 'crit' or 'hyst' string followed by optional 'eresis' string.
                       # '?:' means it is not a numbered capture group.
            \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 followed by one or more
                       # digits. '?:' means it is not a numbered capture group.
            [°\s]C     # Match degree char or space char, and then 'C' char
            )?         # >>>end of group
            \)         # ')' char
            )?         # >>>>end of group
        /xm,           # Use extended regular expressions and treat string as multiple lines.
    title => 'Temperatures',
    vlabel => 'degrees Celsius',
    print_threshold => \&temp_threshold,
    graph_args => '--base 1000 -l 0'
    },

    volt => {
        regex => qr/
            ^           # String must start with:
            ([^:\n]*)   # Match any non-whitespace char, except ':' and new line
                        # one space in front of them.
                        # (match the label as \$1)

            \s*:\s*     # Zero or more spaces followed by ':' and
                        # 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 followed by one or more digits.
                        # '?:' means it is not a numbered capture group.
            \sV         # one space, 'V' char

            (?:         # >>>>Match the following statement.
                        # '?:' means it is not a numbered capture group.
            \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 followed by one or more
                        # digits. '?:' means it is not a numbered capture group.
            \sV,\s*     # One space char, 'V,' string followed 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 followed by one or more digits.
                        # '?:' means passive group (does not match)
            \sV         # one space, 'V' char
            \)          # ')' char
            )?          # >>>>end of statement
        /xm,            # Use extended regular expressions and treat string as multiple lines.

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

    power => {
        regex => qr/
            ^           # String must start with:
            ([^:\n]*)   # Match any non-whitespace char, except ':' and new line
                        # one space in front of them.
                        # (match the label as \$1)

            \s*:\s*     # Zero or more spaces followed by ':' and
                        # zero or more spaces followed by
            (\d+        # Match one or more digits
                        # (match the power as \$2) followed by
            (?:\.\d+)?) # Zero or one match of '.' char followed by one or more digits.
                        # '?:' means it is not a numbered capture group.
            \sW         # one space, 'W' char

            (?:         # >>>>Match the following statement.
                        # '?:' means it is not a numbered capture group.
            \s+         # One or more space followed by
            \(          # '(' char
            crit        # Match crit string
            \s*=\s*     # Match zero or more spaces and then '=' char and
                        # then zero or more spaces, followed by
            (\d+         # Match one or more digits
                        # (Match the minimum value as \$3) followed by
            (?:\.\d+)?) # Zero or one match of '.' char followed by one or more
                        # digits. '?:' means it is not a numbered capture group.
            \sW\s*     # One space char, 'W' character followed by zero or
                        # more spaces
            \)          # ')' char
            )?          # >>>>end of statement
        /xm,            # Use extended regular expressions and treat string as multiple lines.

        title => 'Power',
        vlabel => 'Watts',
        print_threshold => \&power_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_vlabel $config{$func}->{vlabel}\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;

  if (defined($min) and defined($max) and $min > $max) {
      ($min, $max) = ($max, $min);
  }

  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";
}

sub power_threshold {
  my $name = shift;
  my $crit = shift;
  my $warn_percent = exists $ENV{watt_warn_percent} ? $ENV{watt_warn_percent} : 20;

  return unless defined ($crit);

  printf "$name.warning :%.2f\n", $crit * (100-$warn_percent)/100;
  printf "$name.critical :$crit\n";
}

# vim:syntax=perl
