#!/usr/bin/perl -w

# check_cdu.pl Written by Eric Schoeller for the University of Colorado Boulder - 20130121
my $VERSION="1.4";
my $SCRIPT_NAME="check_cdu.pl";

# 20140719: Made some minor modifications to better handle Neutral phases on 400v units.
# 20140727: Added nvmFail, outOfBalance to towerStatus. Added reading,offFuse,onFuse to infeedStatus.
# 20141128: Added dewpoint functionality, code contributions from Mike Burr
# 20150819: Fixed Voltage Imbalance and Load Imbalance checks for 400v units which have a Neutral.

use Net::SNMP;
use Getopt::Std;
use Getopt::Long;
use Nagios::Plugin::Threshold;
use strict;

# Most functionality of this program occurs in the the global scope, very few functions
# pass or accept arguments. This means that there are a vast number of required global
# variables that need to be defined here.
my (@UNKNOWN, @CRITICAL, @WARNING, @OK);
my ($verbose, $timeout, $port, $help, $community, $host, $oksummary, $warning_string, $critical_string, $fahrenheit, $celsius, $temp, $humid, $dewtemp, $dewdelta, $ths, $version);
my ($systemVersion, $systemNICSerialNumber, $systemLocation, $systemTowerCount, $systemEnvMonCount, $systemTotalPower, $systemPowerFactor);
my (@crit_strings, @warn_strings, @threshold_array);
my ($temp_threshold, $humid_threshold, $dewpoint_threshold, $user_temp_threshold_set, $user_humid_threshold_set, $user_dewpoint_threshold_set, @sensors, @ths_list);
my ($tower, @towers, @tower_queries, $infeed, @infeeds, @infeed_queries);
my (%sensor_data, %tower_data, %infeed_data);

#
# Constant Definition
# 0 <= T <= 50 C constants from J Applied Meteorology and Climatology
#

my $const_a = 6.1121;
my $const_b = 17.368;
my $const_c = 238.88;

GetOptions ("C=s"	=>	\$community,
      "H=s"		=>	\$host,
      "verbose"		=>	\$verbose,
      "timeout=i"	=>	\$timeout,
      "port=i"		=>	\$port,
      "help"		=>	\$help,
      "oksummary"	=>	\$oksummary,
      "warning=s"	=>	\$warning_string,
      "critical=s"	=>	\$critical_string,
      "fahrenheit"	=>	\$fahrenheit,
      "celsius"		=>	\$celsius,
      "temp"		=>	\$temp,
      "humid"		=>	\$humid,
      "dewtemp"		=>	\$dewtemp,
      "dewdelta"	=>	\$dewdelta,
      "tower:s"		=>      \$tower,
      "infeed:s"	=>	\$infeed,
      "ths=s"		=>	\$ths,
      "version"		=>	\$version
      );

if (! $host || ! $community) {
   usage();
}

if ($help) {
   usage();
}

if ($version) {
   version();
}

if (! $timeout) {
   $timeout = 2;
}

if (! $port) {
   $port = 161;
}

if (defined($dewtemp) && defined($dewdelta)) {
   &exit_unknown("UNKNOWN - Combining dewpoint temperature and delta in the same plugin invocation is not permitted");
}


####################
### MAIN PROGRAM ###
####################

# Fire up an SNMP session
my ($session, $error) = Net::SNMP->session (
      hostname     =>      $host,
      community    =>      $community,
      version      =>      'snmpv2c',
      timeout      =>      $timeout,
      port         =>      $port,  
      );

if ( !defined $session ) {
   &exit_unknown("UNKNOWN - SNMP ERROR: $error");
}


### Basic System
&global;

### Environment
if (defined($temp) || defined($humid) || defined($dewtemp) || defined($dewdelta)) {


   if(defined($ths)) {
      # This is a multi-part sensor list
      if ($ths =~ /,/) {
         @ths_list = split(',', $ths);
      }

      # Single-part sensor list, single element array
      else {
         push(@ths_list, $ths);
      }

      # Do some simple verification of the sensor ID. Generally you can only have
      # A1, A2, B1, B2 .. at least in my experience. I 'future-proofed' it by 
      # allowing for a whole crap load of sensors. We do a *real* check for sensor
      # validity later, but there is no sense in continuing if the user really provided
      # complete garbage.
      foreach my $ths (@ths_list) {
         if (! ($ths =~ /[A-Z][1-9]/) || length($ths) > 2) {
            &exit_unknown("UNKNOWN - Basic sensor ID verification failed for '$ths'. If this an error, read the docs, change the regex");
         }
      }
   } 

   &sanitize_set_user_thresholds;
   &build_sensor_data;
   &process_sensor_data;
}

### Tower
if (defined($tower)) {

   &build_tower_data;
   &sanitize_tower_set_thresholds;
   &process_tower_data;


}

### Infeed
if (defined($infeed)) {

   &build_infeed_data;
   &sanitize_infeed_set_thresholds;
   &process_infeed_data;
}

### Termination
$session->close();
&plugin_exit;




#################
### Functions ###
#################

# Any conditions that should trigger premature program termination
# generally use this function. The function accepts a string argument 
# containing the desired exit message, however this is not required. 
# If an SNMP session happens to exist, it is also closed.
sub exit_unknown() {
   my $exit_msg;
   if (defined(@_)) {
      $exit_msg = "@_";
   }
   else {
      $exit_msg = "UNKNOWN - unhandled exception";
   }

   # Always do our best to close out SNMP sessions
   if (defined($session)) {
      $session->close();
   }

   print "$exit_msg\n";
   exit(3);

}


# Print usage information to the terminal
sub usage {
   print "check_cdu.pl -H <host> -C <community> [-t SNMP timeout] [-p SNMP port] <additional options>\n\n";
   print "Additional Options:\n";
   print "\t--temp\n";
   print "\t   Report temperature readings from sensors being queried\n\n";
   print "\t--humid\n";
   print "\t   Report humidity readings from sensors being queried\n\n";
   print "\t--dewtemp\n";
   print "\t   Report dewpoint temperature readings from sensors being queried\n\n";
   print "\t--dewdelta\n";
   print "\t   Report dewpoint temperature delta from sensors being queried\n\n";
   print "\t--ths\n";
   print "\t   Specify a list of temperature/humidity probes to query by ID (ie. A1,B2)\n\n";
   print "\t--oksummary\n";
   print "\t   Output a summary for OK status instead of individual sensor readings. Examples:\n";
   print "\t   With Summary: OK - Sentry Switched CDU Version 7.0e, Location: LOCATION, 2 metrics are OK\n";
   print "\t   Without: OK - LOCATION, Bottom-Rack-Inlet_F31(A1): 66.2F Bottom-Rack-Inlet_F31(A1): 40%\n\n";
   print "\t--fahrenheit\n";
   print "\t   Will convert any sensor readings that are in Celsius to Fahrenheit.\n";
   print "\t   This will also convert automatic thresholds. See documentation for further information.\n\n";
   print "\t--celsius\n";
   print "\t   Will convert any sensor readings that are in Fahrenheit to Celsius.\n";
   print "\t   This will also convert automatic thresholds. See documenation for further information.\n\n";
   print "\t--warning\n";
   print "\t   This accepts a standard nagios plugin compliant range string. Separate thresholds can be\n";
   print "\t   chained together (ie. 50:80,10:90) when querying for both temperature and humidity concurrently\n";
   print "\t   The temperature threshold is listed first, then humidity threshold. \n";
   print "\t   See the plugin documentation and EXAMPLES section for detailed information!!\n\n";
   print "\t--critical\n";
   print "\t   Same as --warning , however applies critical thresholds instead. Please read the documentation.\n\n";
   print "\t--tower\n";
   print "\t   With no arguments, simply checks tower 'Status'. Can also query other metrics. Mostly useful on PIPS units.\n";
   print "\t   The following list of metrics are currently available:\n";
   print "\t   InfeedCount,VACapacity,VACapacityUsed,ActivePower,ApparentPower,PowerFactor,Energy,LineFrequency\n";
   print "\t   Thresholds are chained together in order, similar to temp/humid. Read documentation or see EXAMPLES\n\n";
   print "\t--infeed\n";
   print "\t   With no arguments, simply checks infeed 'Status' and 'LoadStatus'. Can also query other metrics.\n";
   print "\t   The following list of metrics are currently available:\n";
   print "\t   LoadValue,CapacityUsed,PhaseCurrent,Power,CrestFactor,PowerFactor,ApparentPower,Voltage,Energy\n";
   print "\t   Additionally, LoadImbalance and VoltageImbalance are included as well. Read the docs for more information\n\n";
   print "\t--help\n";
   print "\t   This message. Please issue a 'perldoc $SCRIPT_NAME' for the full documentation.\n\n";
   print "\t--version\n";
   print "\t   Print version information.\n\n\n";
   print "EXAMPLES FOR THE IMPATIENT PEOPLE:\n";
   print "\$ check_cdu.pl -H 192.168.0.1 -C public --temp --humid --fahrenheit --ths A1,A2 --warning 60:85,40:60 --critical 95,15:\n";
   print "\$ check_cdu.pl -H 192.168.0.1 -C public --tower VACapacityUsed,ApparentPower,LineFrequency,VACapacity --warning 44,4999,60.4,20000\n";
   print "\$ check_cdu.pl -H 192.168.0.1 -C public --infeed LoadValue,CapacityUsed,Voltage,LoadImbalance --critical 16,60,230,45\n\n";
   print "This accounts for the basic operation guidelines for this plugin. Invocation can be rather complex.\n";
   print "It is HIGHLY recommended that you review the full documentation (perldoc $SCRIPT_NAME). Thank you.\n\n";  
   exit(3);
}

# Standard version information
sub version {
   print <<"EOF";
   This is version $VERSION of $SCRIPT_NAME.

   Copyright (c) 2013 Eric Schoeller (eric.schoeller <at> coloradoDOTedu).
   All rights reserved.

   This module is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License.
   See http://www.fsf.org/licensing/licenses/gpl.html

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

EOF
exit 3;

}


# Dew Point Calculation and Temperature Conversion functions
# Mike Burr (michael.burr@colorado.edu)
# November 2014

#
# Temperature conversion functions
#

# C to F conversion
# Call as t_convert_c_to_f( scalar t) where t is numeric temperature in C
# Returns numeric scalar
sub t_convert_c_to_f {
   my ($t_in) = @_;
   return (9.0/5.0)*$t_in + 32.0;
}

# C to F conversion
# Call as t_convert_f_to_c( scalar t) where t is numeric temperature in F
# Returns numeric scalar
sub t_convert_f_to_c {
   my ($t_in) = @_;
   return (5.0/9.0)*($t_in - 32.0);
}

#
# Dew Point Calculation
# call as t_dewpoint(scalar t, scalar rh) 
#    where t is numeric temperature in C
#          and rh is numeric %relative humidity > 0
# ex. dew point for 10 degrees C and 40% RH
#     t_dewpoint(10.0, 40.0)
sub t_dewpoint {
   my ($t, $rh) = @_;
   my $pa_est = t_dewpoint_pa_est($t,$rh);
   return ($const_c * log($pa_est/$const_a)) / ($const_b - log ($pa_est / $const_a));
}

#
# Helper functions for t_dewpoint
#

#
# Estimate the actual water vapor pressure P_a
#

sub t_dewpoint_pa_est {
   my ($t, $rh) = @_;
   return $const_a * exp(t_dewpoint_gamma($t,$rh));
}

#
# Estimate for gamma(t,rh)
#

sub t_dewpoint_gamma {
   my ($t, $rh) = @_;
   return log($rh/100.0) + ($const_b * $t / ($const_c + $t));
}

#
# Temperature Conversion Test Functions
#

sub test_t_convert_c_to_f {
   (print "t_convert_c_to_f failed" && die) unless (t_convert_c_to_f(0.0) == 32.0);
   (print "t_convert_c_to_f failed" && die) unless (t_convert_c_to_f(10.0) == 50.0);
   (print "t_convert_c_to_f failed" && die) unless (t_convert_c_to_f(40.0) == 104.0);
   (print "t_convert_c_to_f failed" && die) unless (t_convert_c_to_f(80.0) == 176.0);
   (print "t_convert_c_to_f failed" && die) unless (t_convert_c_to_f(100.0) == 212.0);
}

sub test_t_convert_f_to_c {
   (print "t_convert_f_to_c failed" && die) unless (t_convert_f_to_c(32.0) == 0.0);
   (print "t_convert_f_to_c failed" && die) unless (t_convert_f_to_c(50.0) == 10.0);
   (print "t_convert_f_to_c failed" && die) unless (t_convert_f_to_c(104.0) == 40.0);
   (print "t_convert_f_to_c failed" && die) unless (t_convert_f_to_c(176.0) == 80.0);
   (print "t_convert_f_to_c failed" && die) unless (t_convert_f_to_c(212.0) == 100.0);
}

#
# Dew Point Calculation Test Functions
#

sub test_t_dewpoint {
   (print "t_dewpoint failed" && die) unless ((sprintf "%.1f" , t_dewpoint(10.0,10.0)) == -20.2);
   (print "t_dewpoint failed" && die) unless ((sprintf "%.1f" , t_dewpoint(20.0,53.0)) == 10.1);
}



#
# Main program body - test harness for functions
#

# test_t_convert_c_to_f();
# test_t_convert_f_to_c();
# test_t_dewpoint();


## End dewpoint code.

# Fetch all system wide information and set appropriate global scope variables.
# Function takes no arguments, and returns nothing. Will handle SNMP exceptions
# internally and handle nagios exit and program termination.
sub global {

   # Fetch generic system information via SNMP
   my $systemTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.1718.3.1');

   if (!defined $systemTable) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   $systemVersion = $systemTable->{'.1.3.6.1.4.1.1718.3.1.1.0'};
   $systemNICSerialNumber = $systemTable->{'.1.3.6.1.4.1.1718.3.1.2.0'};
   $systemLocation = $systemTable->{'.1.3.6.1.4.1.1718.3.1.3.0'};
   $systemTowerCount = $systemTable->{'.1.3.6.1.4.1.1718.3.1.4.0'};
   $systemEnvMonCount = $systemTable->{'.1.3.6.1.4.1.1718.3.1.5.0'};
   $systemTotalPower = $systemTable->{'.1.3.6.1.4.1.1718.3.1.6.0'};

}

sub build_infeed_data() {

   # Get a short list of the available infeeds. This just makes my life easier.
   my $infeed = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.2.1.2');

   if (!defined $infeed) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   # Now get the entire infeed table. This includes what I already got. I don't care.
   my $infeedTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.2.1');

   if (!defined $infeedTable) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   # Find the child OID suffix for each infeed. This is somewhat unneccessary.
   foreach my $key (sort (keys %{$infeed})) {
      $key =~ /.1.3.6.1.4.1.1718.3.2.2.1.2(.*)/;
      my $infeed_id = $1;
      push(@infeeds, $infeed_id);
   }

   foreach my $infeed (@infeeds) {
      while (my ($key, $value) = each %{$infeedTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.2' . $infeed) ) { $infeed_data{$infeed}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.3' . $infeed) ) { $infeed_data{$infeed}->{'Name'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.4' . $infeed) ) { $infeed_data{$infeed}->{'Capabilities'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.5' . $infeed) ) {
            my @val_array = qw(off on offWait onWait offError onError noComm reading offFuse onFuse);
            $infeed_data{$infeed}->{'Status'} = &translate(\@val_array,$value);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.6' . $infeed) ) {
            my @val_array = qw(normal notOn reading loadLow loadHigh overLoad readError noComm);
            $infeed_data{$infeed}->{'LoadStatus'} = &translate(\@val_array,$value);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.7' . $infeed) ) {
            my $real_value = ($value / 100);
            $infeed_data{$infeed}->{'LoadValue'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.8' . $infeed) ) { $infeed_data{$infeed}->{'LoadHighThresh'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.9' . $infeed) ) { $infeed_data{$infeed}->{'OutletCount'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.10' . $infeed) ) { $infeed_data{$infeed}->{'Capacity'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.11' . $infeed) ) {
            my $real_value = ($value / 10);
            $infeed_data{$infeed}->{'Voltage'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.12' . $infeed) ) { $infeed_data{$infeed}->{'Power'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.13' . $infeed) ) { $infeed_data{$infeed}->{'ApparentPower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.14' . $infeed) ) {
            my $real_value = ($value / 100);
            $infeed_data{$infeed}->{'PowerFactor'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.15' . $infeed) ) {
            my $real_value = ($value / 10);
            $infeed_data{$infeed}->{'CrestFactor'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.16' . $infeed) ) {
            my $real_value = ($value / 10);
            $infeed_data{$infeed}->{'Energy'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.17' . $infeed) ) {
            my @val_array = qw(unknown capacitive inductive resistive);
            $infeed_data{$infeed}->{'Reactance'} = &translate(\@val_array,$value);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.18' . $infeed) ) {
            my $real_value = ($value / 10);
            $infeed_data{$infeed}->{'PhaseVoltage'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.19' . $infeed) ) {
            my $real_value = ($value / 100);
            $infeed_data{$infeed}->{'PhaseCurrent'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.20' . $infeed) ) {
            my $real_value = ($value / 10);
            $infeed_data{$infeed}->{'CapacityUsed'} = $real_value;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.21' . $infeed) ) { $infeed_data{$infeed}->{'LineID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.22' . $infeed) ) { $infeed_data{$infeed}->{'LineToLineID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.23' . $infeed) ) { $infeed_data{$infeed}->{'PhaseID'} = $value }

      }
   }
}

sub build_tower_data() {

   # Get a short list of the available towers. This just makes my life easier.
   my $tower = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.1.1.2');

   if (!defined $tower) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   # Now get the entire tower table. This includes what I already got. I don't care.
   my $towerTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.1.1');

   if (!defined $tower) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   # Find the child OID suffix for each tower. This is somewhat unneccessary. 
   foreach my $key (sort (keys %{$tower})) {
      $key =~ /.1.3.6.1.4.1.1718.3.2.1.1.2(.*)/;
      my $tower_id = $1;
      push(@towers, $tower_id);
   }

   foreach my $tower (@towers) {
      while (my ($key, $value) = each %{$towerTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.2' . $tower) ) { $tower_data{$tower}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.3' . $tower) ) { $tower_data{$tower}->{'Name'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.4' . $tower) ) { 
            my @val_array = qw(normal noComm fanFail overTemp nvmFail outOfBalance);
            $tower_data{$tower}->{'Status'} = &translate(\@val_array, $value);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.5' . $tower) ) { $tower_data{$tower}->{'InfeedCount'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.6' . $tower) ) { $tower_data{$tower}->{'ProductSN'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.7' . $tower) ) { $tower_data{$tower}->{'ModelNumber'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.8' . $tower) ) { $tower_data{$tower}->{'Capabilities'} = $value }

         # PIPS OIDs. Should not need any error checking here.

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.9' . $tower) ) { $tower_data{$tower}->{'VACapacity'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.10' . $tower) ) { 
            my $real_va = ($value /10); 
            $tower_data{$tower}->{'VACapacityUsed'} = $real_va;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.11' . $tower) ) { $tower_data{$tower}->{'ActivePower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.12' . $tower) ) { $tower_data{$tower}->{'ApparentPower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.13' . $tower) ) { 
            my $real_pf = ($value / 100);
            $tower_data{$tower}->{'PowerFactor'} = $real_pf;
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.14' . $tower) ) { $tower_data{$tower}->{'Energy'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.1.1.15' . $tower) ) { $tower_data{$tower}->{'LineFrequency'} = $value }
      }
   }
}



# Fetch all temperature / humidity data via SNMP and restructure the flat data into
# a multi dimensional hash. All data is manipulated in the global scope, so nothing is
# passed in or out of this function. If faults occur, it handles appropriate Nagios 
# exit codes and program termination.
sub build_sensor_data {

   # Get just the list of sensors on the CDU.
   my $tempHumidSensor = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.5.1.2');

   if (!defined $tempHumidSensor) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   # Get the entire table.
   my $tempHumidTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.1718.3.2.5.1');

   if (!defined $tempHumidTable) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }

   # Find the child OID suffix of each sensor.
   foreach my $key (sort (keys %{$tempHumidSensor})) {

      $key =~ /.1.3.6.1.4.1.1718.3.2.5.1.2(.*)/;
      my $sensor_id = $1;
      push(@sensors, $sensor_id);
   }   

   foreach my $sensor (@sensors) {

      while (my ($key, $value) = each %{$tempHumidTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.2' . $sensor) ) { $sensor_data{$sensor}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.3' . $sensor) ) { $sensor_data{$sensor}->{'Name'} = $value }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.4' . $sensor) ) { 
            my @val_array = qw(found notFound lost noComm);
            $sensor_data{$sensor}->{'Status'} = &translate(\@val_array, $value);
         }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.5' . $sensor) ) { 
            my @val_array = qw(normal notFound reading tempLow tempHigh readError lost noComm);
            $sensor_data{$sensor}->{'TempStatus'} = &translate(\@val_array, $value);
         }


         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.6' . $sensor) ) { 
            # Do the temperature division here.
            my $real_temp = ($value / 10);
            $sensor_data{$sensor}->{'TempValue'} = $real_temp;
         }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.7' . $sensor) ) { $sensor_data{$sensor}->{'TempLowThresh'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.8' . $sensor) ) { $sensor_data{$sensor}->{'TempHighThresh'} = $value }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.9' . $sensor) ) { 
            my @val_array = qw(normal notFound reading humidLow humidHigh readError lost noComm);
            $sensor_data{$sensor}->{'HumidStatus'} = &translate(\@val_array, $value);
         }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.10' . $sensor) ) { $sensor_data{$sensor}->{'HumidValue'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.11' . $sensor) ) { $sensor_data{$sensor}->{'HumidLowThresh'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.12' . $sensor) ) { $sensor_data{$sensor}->{'HumidHighThresh'} = $value }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.13' . $sensor) ) { 
            my @val_array = qw(celsius fahrenheit);
            $sensor_data{$sensor}->{'TempScale'} = &translate(\@val_array, $value);
         }

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.14' . $sensor) ) { $sensor_data{$sensor}->{'TempRecDelta'} = $value }

      }
   }
}


# Translate numerical SNMP integer response to textual equivalent given
# a static translation array derived from the MIB file. 
sub translate() {

   my ($val_array , $value) = @_;
   my $value2 = "translateError";
   for (my $i=0; $i < scalar(@$val_array); $i++) {
      if ($i eq $value) {
         $value2 = (@$val_array[$i] . "(" . $i . ")");
      }
   }
   return $value2;

}


sub process_infeed_data() {

   my $unit;
   foreach my $infeedid (@infeeds) {

      # This is a bare request with no arguments. Do the standard checks.
      if ($infeed_queries[0] eq '') {

         my $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . "Status: " . $infeed_data{$infeedid}->{'Status'};
         # Check the general status of the infeed.
         if ($infeed_data{$infeedid}->{'Status'} =~ /\bnoComm\b|\boffWait\b|\bonWait\b|\boff\b|\breading\b/) {
            push(@WARNING,$msg_txt);
         }
         elsif ($infeed_data{$infeedid}->{'Status'} =~ /\boffError\b|\bonError\b|\boffFuse\b|\bonFuse\b/) {
            push(@CRITICAL,$msg_txt);
         }

         # Status is Normal. Proceed.
         else {
            push(@OK,$msg_txt);
         }

         $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . "LoadStatus: " . $infeed_data{$infeedid}->{'LoadStatus'};
         # Check the Load Status of the Infeed.
         if ($infeed_data{$infeedid}->{'LoadStatus'} =~ /\bnoComm\b|\breading\b|\bloadLow\b/) {
            push(@WARNING,$msg_txt);
         }
         elsif ($infeed_data{$infeedid}->{'LoadStatus'} =~ /\bnotOn\b|\bloadHigh\b|\boverLoad\b|\breadError\b/) {
            push(@CRITICAL,$msg_txt);
         }

         # Status is Normal. Proceed.
         else {
            push(@OK,$msg_txt);
         }
      }

      # There are some queries to run.
      else {
         for (my $i=0; $i < scalar(@infeed_queries); $i++) {

            # If the infeed has noComm, bug out with an UNKNOWN and skip any checks for the infeed.
            # They will all be false anyway. We use UNKNOWN here, because it is assumed that
            # another check is already being done on just the infeed status, and if multiple other
            # metrics are being checked, it doesn't make sense to generate too many alarms for a single noComm event.
            if ($infeed_data{$infeedid}->{'Status'} =~ /noComm/) {
               my $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . "Status: " . $infeed_data{$infeedid}->{'Status'};
               push(@UNKNOWN,$msg_txt);
            }

            else {

               my ($infeed_status, $msg_txt);

               # Handle the Balance checks separately.
               if ($infeed_queries[$i] =~ /\bLoadImbalance\b|\bVoltageImbalance\b/) {
                  # Filter out the tower ID from this particular infeed.
                  $infeedid =~ /\.(\d*).\d/;
                  my $towerid = $1;

                  my ($total, $counter, $infeedvalue, $deviation, $imbalance);
                  # Find all the associated infeeds that go along with this infeed on this particular tower. Ignore any of the 4th or "D" infeeds,
                  # since these are the neutral lines which we aren't concerned with from a Load Imbalance perspective.
                  foreach my $infeedid2 (@infeeds) {
                     if (($infeedid2 =~ /\.$towerid.\d/) && (!($infeed_data{$infeedid2}->{'ID'} =~ /\bAD\b|\bBD\b|\bCD\b|\bDD\b/)) && (!($infeed_data{$infeedid}->{'ID'} =~ /\bAD\b|\bBD\b|\bCD\b|\bDD\b/))) {
                        $counter++;
                        # We have found an infeed that is on the same tower as the infeed in question (or the infeed in question itself!)
                        if ($infeed_queries[$i] =~ /\bLoadImbalance\b/) {
                           if (! defined($infeed_data{$infeedid2}->{'LoadValue'}) ) {
                              &exit_unknown("UNKNOWN - Cannot compute LoadImbalance, LoadValue metric missing.");
                           }
                           # I would consider ignoring infeeds with very little load right here, but I'd have to find a way
                           # to only ignore the entire infeed if it was completely unloaded.
                           $total += $infeed_data{$infeedid2}->{'LoadValue'};
                           if ($infeedid eq $infeedid2) { $infeedvalue = $infeed_data{$infeedid2}->{'LoadValue'} }
                        }

                        if ($infeed_queries[$i] =~ /\bVoltageImbalance\b/) {
                           if (! defined($infeed_data{$infeedid2}->{'Voltage'}) ) {
                              &exit_unknown("UNKNOWN - Cannot compute VoltageImbalance, Voltage metric missing.");
                           }
                           $total += $infeed_data{$infeedid2}->{'Voltage'};
                           if ($infeedid eq $infeedid2) { $infeedvalue = $infeed_data{$infeedid2}->{'Voltage'} }
                        }
                     }
                  }
                  # Skip the 4th phase, or the Neutral, typically found on 400v units.
                  if (!($infeed_data{$infeedid}->{'ID'} =~ /\bAD\b|\bBD\b|\bCD\b|\bDD\b/)) {
                     # This is only supported on 3 phase units. Exit now if we don't have 3 phases (infeeds).
                     if ($counter < 3) {
                        &exit_unknown("UNKNOWN - Only detected $counter infeeds on tower $towerid. $infeed_queries[$i] Only supported on 3 phase units.");
                     }
                     my $average = ($total / $counter);
                     if ($average > $infeedvalue) { $deviation = ($average - $infeedvalue) }
                     elsif ($average < $infeedvalue) { $deviation = ($infeedvalue - $average) }
                     else { $deviation = 0 }
                  
                     # Avoid pesky divide by zero errors :) This is most likely a tower with no load.
                     if ($average > 0 ) {
                        $imbalance = sprintf("%.2f",(($deviation / $average) * 100));
                     }
                     # Assume that there is no imbalance, since the average is less than 0. 
                     else {
                        $imbalance = 0;
                     }
                     $infeed_status = $threshold_array[$i]->get_status($imbalance);
                     $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . $infeed_queries[$i] .": " . $imbalance . "%";
                  }
               }
               # Now it is very simple to bug out if the user is querying a metric that the CDU 
               # does not support. Chances are this is not a PIPS unit. I should be parsing the 
               # Capabilities BIT to determine this, but I am super lazy about that.
               elsif (!defined($infeed_data{$infeedid}->{$infeed_queries[$i]})) {
                  &exit_unknown("UNKNOWN - It appears that infeed metric '$infeed_queries[$i]' is not available on this unit");
               }

               # Set the appropriate units given the type of metric.
               # This is just simply makes it easier to understand the data in the plugin output
               # These are the standardized symbols according to wikipedia!
               if ($infeed_queries[$i] =~ /\bPhaseVoltage\b|\bVoltage\b/) { $unit = "V" }
               elsif ($infeed_queries[$i] =~ /\bCapacityUsed\b/) {  $unit = "%" }
               elsif ($infeed_queries[$i] =~ /\bPower\b/) { $unit ="W" }
               elsif ($infeed_queries[$i] =~ /\bApparentPower\b/) { $unit ="VA" }
               elsif ($infeed_queries[$i] =~ /\bEnergy\b/) { $unit = "kWh" }
               elsif ($infeed_queries[$i] =~ /\bLoadValue\b|\bPhaseCurrent\b/) { $unit = "A" }
               else { $unit = "" }

               # Use the PhaseID instead of ID in the output
               if ($infeed_queries[$i] =~ /\bPhaseVoltage\b|\bPhaseCurrent\b/) {

                  # If there is no PhaseID set, disregard checking it. This typically occurs on the Neutral phase for a 400v unit.
                  if ($infeed_data{$infeedid}->{'PhaseID'}) {
                     $infeed_status = $threshold_array[$i]->get_status($infeed_data{$infeedid}->{$infeed_queries[$i]});
                     $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'PhaseID'} . ") " . $infeed_queries[$i] .": " . $infeed_data{$infeedid}->{$infeed_queries[$i]} . $unit;
                  }
               }

               # Handle Voltage carefully. 400v units will report a 4th phase as the neutral with a -0.1v value. 
               # We handle this exception here by ignoring any Line ID ending in 'D'. I went ahead and included 4 towers,
               # even though we have only ever seen 2. Subsequent towers would be ED, FD, etc.
               elsif ($infeed_queries[$i] =~ /\bVoltage\b/) {
                  if (!($infeed_data{$infeedid}->{'ID'} =~ /\bAD\b|\bBD\b|\bCD\b|\bDD\b/)) {
                     $infeed_status = $threshold_array[$i]->get_status($infeed_data{$infeedid}->{$infeed_queries[$i]});
                     $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . $infeed_queries[$i] .": " . $infeed_data{$infeedid}->{$infeed_queries[$i]} . $unit;
                  }
               }

               # An unloaded infeed will generally report -0.01 as the PowerFactor. I am not entirely interested to 
               # receive an alarm about this condition. So, as long as the reported PowerFactor is greater than 0 
               # the default logic will be applied. If the PowerFactor is less than zero simply set the state to OK
               # and report the negative value in the output. 
               elsif ($infeed_queries[$i] =~ /\bPowerFactor\b/) {
                  if ($infeed_data{$infeedid}->{$infeed_queries[$i]} > 0) {
                     $infeed_status = $threshold_array[$i]->get_status($infeed_data{$infeedid}->{$infeed_queries[$i]});
                     $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . $infeed_queries[$i] .": " . $infeed_data{$infeedid}->{$infeed_queries[$i]} . $unit;
                  }
                  else {
                     $infeed_status = 0;
                     $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . $infeed_queries[$i] .": " . $infeed_data{$infeedid}->{$infeed_queries[$i]} . $unit;
                  }
               }
               
               # Essentially everything else.
               elsif (!($infeed_queries[$i] =~ /\bLoadImbalance\b|\bVoltageImbalance\b/)) {
                  $infeed_status = $threshold_array[$i]->get_status($infeed_data{$infeedid}->{$infeed_queries[$i]});
                  $msg_txt = $infeed_data{$infeedid}->{'Name'} . "(" . $infeed_data{$infeedid}->{'ID'} . ") " . $infeed_queries[$i] .": " . $infeed_data{$infeedid}->{$infeed_queries[$i]} . $unit;
               }

               # There are cases when infeed_status won't be set because we have skipped evaluating a metric due to other reasons. 
               if (defined($infeed_status)) {
                  if ($infeed_status eq 0) { push(@OK,$msg_txt) }
                  if ($infeed_status eq 1) { push(@WARNING,$msg_txt) }
                  if ($infeed_status eq 2) { push(@CRITICAL,$msg_txt) }
               }
            }
         }
      }
   }
}


sub process_tower_data() {

   my $unit;
   foreach my $towerid (@towers) {
      # Standard message text for most alerts.
      my $msg_txt = $tower_data{$towerid}->{'Name'} . "(" . $tower_data{$towerid}->{'ID'} . ") " . "Status: " . $tower_data{$towerid}->{'Status'};

      # This is a bare request with no arguments. Do the standard checks.
      if ($tower_queries[0] eq '') {
         # If the tower is beat up, it's sensical to report that now, and skip tossing unitialized erros below.
         if ($tower_data{$towerid}->{'Status'} =~ /\bnoComm\b|\boutOfBalance\b/) { push(@WARNING,$msg_txt) }
         elsif ($tower_data{$towerid}->{'Status'} =~ /\bfanFail\b|\boverTemp\b|\bnvmFail\b/) { push(@CRITICAL,$msg_txt) }
         else { push(@OK,$msg_txt) }
      } 

      # There are some queries to run.
      else {
         for (my $i=0; $i < scalar(@tower_queries); $i++) {

            # If the tower has noComm, bug out with an UNKNOWN and skip any checks for the tower.
            # They will all be false anyway. We use UNKNOWN here, because it is assumed that
            # another check is already being done on just the tower status, and if multiple other
            # metrics are being checked, it doesn't make sense to generate too many alarms for a single noComm event.
            if ($tower_data{$towerid}->{'Status'} =~ /noComm/) { push(@UNKNOWN,$msg_txt) }

            else {
               # Now it is very simple to bug out if the user is querying a metric that the CDU 
               # does not support. Chances are this is not a PIPS unit. I should be parsing the 
               # Capabilities BIT to determine this, but I am super lazy about that.
               if (!defined($tower_data{$towerid}->{$tower_queries[$i]})) {
                  &exit_unknown("UNKNOWN - It appears that tower metric '$tower_queries[$i]' is not available on this unit");
               }

               # Set the appropriate units given the type of metric.
               # This is just simply makes it easier to understand the data in the plugin output
               if ($tower_queries[$i] =~ /VACapacity\b|ApparentPower/) { $unit = "VA" }
               elsif ($tower_queries[$i] =~ /VACapacityUsed/) {  $unit = "%" }
               elsif ($tower_queries[$i] =~ /ActivePower/) { $unit ="W" }
               elsif ($tower_queries[$i] =~ /Energy/) { $unit = "kWh" }
               elsif ($tower_queries[$i] =~ /LineFrequency/) { $unit = "Hz" }
               else { $unit = "" }

               my $tower_status = $threshold_array[$i]->get_status($tower_data{$towerid}->{$tower_queries[$i]});
               my $msg_txt = $tower_data{$towerid}->{'Name'} . "(" . $tower_data{$towerid}->{'ID'} . ") " . $tower_queries[$i] .": " . $tower_data{$towerid}->{$tower_queries[$i]} . $unit;
               if ($tower_status eq 0) { push(@OK,$msg_txt) }
               if ($tower_status eq 1) { push(@WARNING,$msg_txt) }
               if ($tower_status eq 2) { push(@CRITICAL,$msg_txt) }
            }
         }
      }
   }      
}




# Process the sensor data collected over SNMP, apply various options and thresholds.
# Determine which sensors should be queried, and record status information.
sub process_sensor_data() {

# Build a list of sensors to query. Also detect if a given sensor is not installed.
   my @sensors_to_query;
   my $found = 0;
   if (@ths_list) {
      foreach my $thsq (@ths_list) {
         $found = 0;
         foreach my $sensor (@sensors) {
            if ($thsq eq $sensor_data{$sensor}->{'ID'} ) {
               push(@sensors_to_query,$sensor);
               $found = 1;
            }
         }
         if(!$found) {
            &exit_unknown("UNKNOWN - Sensor ID $thsq not present!");
         }
      }
   }

   # No $ths was passed at the command line, check all sensors.
   else {
      @sensors_to_query = @sensors;
   } 

   foreach my $sensor (@sensors_to_query) {

      my ($TempLowThresh, $TempHighThresh, $TempValue, $temp_warning_string, $humid_warning_string, $TempUnit, $DewValue, $DewUnit, $dewpoint);

      # Base text to use in all messages
      my $msg_base = $sensor_data{$sensor}->{'Name'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . ": ";

      # Sensor is configured for Celsius, but reporting should be done in Fahrenheit.
      # Use the standard formula to convert the thresholds and temperature value. 
      if ( defined($fahrenheit) && ($sensor_data{$sensor}->{'TempScale'} =~ /celsius/) ) {
         $TempLowThresh = t_convert_c_to_f($sensor_data{$sensor}->{'TempLowThresh'});
         $TempHighThresh = t_convert_c_to_f($sensor_data{$sensor}->{'TempHighThresh'});
         $TempValue = t_convert_c_to_f($sensor_data{$sensor}->{'TempValue'});
         $TempUnit ="F";
      }

      # Sensor is configured for Fahrenheit, but reporting should be done in Celsius.
      # Use the standard formula to convert the thresholds and temperature value.
      elsif ( defined($celsius) && ($sensor_data{$sensor}->{'TempScale'} =~ /fahrenheit/) ) {
         $TempLowThresh = t_convert_f_to_c($sensor_data{$sensor}->{'TempLowThresh'});
         $TempHighThresh = t_convert_f_to_c($sensor_data{$sensor}->{'TempHighThresh'});
         $TempValue = t_convert_f_to_c($sensor_data{$sensor}->{'TempValue'});
         $TempUnit ="C";
      }

     # The requested temperature scale matches the configured temperature scale.
      else {
         $TempLowThresh = $sensor_data{$sensor}->{'TempLowThresh'};
         $TempHighThresh = $sensor_data{$sensor}->{'TempHighThresh'};
         $TempValue = $sensor_data{$sensor}->{'TempValue'};
         if ($sensor_data{$sensor}->{'TempScale'} =~ /celsius/) {
            $TempUnit ="C";
         }
         if ($sensor_data{$sensor}->{'TempScale'} =~ /fahrenheit/) {
            $TempUnit ="F";
         }

      }

      # If a threshold was not defined on the command line, set them automatically here based
      # on the thresholds set on the unit. The CDU only has a HIGH and LOW threshold, and these
      # can only applied to either a WARNING or CRITICAL range, but clearly not both. I chose to
      # use WARNING, since this is fully-auto mode anyway. You can easily change this behavior by
      # replacing the 'warning' with 'critical' in the conditional below.
      if ( !defined($user_temp_threshold_set)  ) {
         $temp_warning_string = $TempLowThresh . ":" . $TempHighThresh;
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds( warning => $temp_warning_string);
      }

      if ( !defined($user_humid_threshold_set) ) {
         $humid_warning_string = $sensor_data{$sensor}->{'HumidLowThresh'} . ":" . $sensor_data{$sensor}->{'HumidHighThresh'};
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds( warning => $humid_warning_string);
      } 

      # The CDU obviously does not have built-in thresholds for dewpoint, so these need to be defined on the commandline.
      if ((defined($dewtemp) || defined($dewdelta))&& !defined($user_dewpoint_threshold_set)) {
         &exit_unknown("UNKNOWN - Commandline thresholds required for dewpoint checks");
      }
         
      # Use the Nagios::Plugin magic to determine the status of each sensor given the thresholds we have constructed.
      my $humid_status = $humid_threshold->get_status($sensor_data{$sensor}->{'HumidValue'});
      my $temp_status = $temp_threshold->get_status($TempValue);



      # Decide to report readings based upon what the user asked for.
      # (It was easy to do all the work one way or another even if we didn't need to) 
      if (defined($temp)) {

         # If the sensor is in an Error state, handle that now. We ignore the LowThresh and HighThresh states. 
         # I use the UNKNOWN state here, because , well , it's unknown. I don't want to get paged unless there 
         # is certainly a problem. We have other methods that will allow us to see that this sensor has failed.
         if ($sensor_data{$sensor}->{'TempStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base . $sensor_data{$sensor}->{'TempStatus'};
            push(@UNKNOWN, $msg_txt);
         }

         # The sensor is most likely in normal(0) state, so report things as normal.
         else {
            my $msg_txt = $msg_base . $TempValue . $TempUnit; 
            if ($temp_status eq 0) { push(@OK,$msg_txt) }
            if ($temp_status eq 1) { push(@WARNING,$msg_txt) }
            if ($temp_status eq 2) { push(@CRITICAL,$msg_txt) }
         }
      }

      if (defined($humid)) {

         # Same as above, if the sensor is in Error, report it differently.
         if ($sensor_data{$sensor}->{'HumidStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base . $sensor_data{$sensor}->{'HumidStatus'};
            push(@UNKNOWN, $msg_txt);
         }

         # Again, report as normal if the sensor is not in error.
         else {
            my $msg_txt = $msg_base . $sensor_data{$sensor}->{'HumidValue'} . "%";
            if ($humid_status eq 0) { push(@OK,$msg_txt) }
            if ($humid_status eq 1) { push(@WARNING,$msg_txt) }
            if ($humid_status eq 2) { push(@CRITICAL,$msg_txt) }
         }
      }

      if (defined($dewtemp) || defined($dewdelta)) {

         # Same as above, if the sensor is in Error, report it differently. Since dewpoint requires both Temp/Humid to calculate we verify
         # that both sensors are operating properly before continuing.
         if ($sensor_data{$sensor}->{'HumidStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base . $sensor_data{$sensor}->{'HumidStatus'};
            push(@UNKNOWN, $msg_txt);
         }

         elsif ($sensor_data{$sensor}->{'TempStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base . $sensor_data{$sensor}->{'TempStatus'};
            push(@UNKNOWN, $msg_txt);
         }

         else {
            # I guess this is where the magic happens.
            if ($sensor_data{$sensor}->{'TempScale'} =~ /celsius/) { 
               $dewpoint = t_dewpoint($sensor_data{$sensor}->{'TempValue'}, $sensor_data{$sensor}->{'HumidValue'});
            }
            elsif ($sensor_data{$sensor}->{'TempScale'} =~ /fahrenheit/) {
               $dewpoint = t_dewpoint(t_convert_f_to_c($sensor_data{$sensor}->{'TempValue'}), $sensor_data{$sensor}->{'HumidValue'});
            }
            # Should never happen, but just in case. 
            else {
               my $msg_txt = $msg_base . " Dewpoint Scale Error";
               push(@UNKNOWN, $msg_txt);
            }

            # Sensor is configured for Celsius, but reporting should be done in Fahrenheit.
            # Use the standard formula to convert the thresholds and temperature value. 
            if ( defined($fahrenheit) && ($sensor_data{$sensor}->{'TempScale'} =~ /celsius/) ) {
               $DewValue = t_convert_c_to_f($dewpoint);
               $TempValue = t_convert_c_to_f($sensor_data{$sensor}->{'TempValue'});
               $DewUnit ="F";
            }

            # Sensor is configured for Fahrenheit, but reporting should be done in Celsius.
            # Use the standard formula to convert the thresholds and temperature value.
            elsif ( defined($celsius) && ($sensor_data{$sensor}->{'TempScale'} =~ /fahrenheit/) ) {
               $DewValue = t_convert_f_to_c($dewpoint);
               $TempValue = t_convert_f_to_c($sensor_data{$sensor}->{'TempValue'});
               $DewUnit ="C";
            }

            # The requested temperature scale matches the configured temperature scale.
            else {
               $DewValue = $dewpoint;
               $TempValue = $sensor_data{$sensor}->{'TempValue'};
               if ($sensor_data{$sensor}->{'TempScale'} =~ /celsius/) {
                  $DewUnit ="C";
               }
               if ($sensor_data{$sensor}->{'TempScale'} =~ /fahrenheit/) {
                  $DewUnit ="F";
               }
            }

            my $dewpoint_status;
            if (defined($dewtemp)) {
               $dewpoint_status = $dewpoint_threshold->get_status($DewValue);
               my $msg_txt = $sensor_data{$sensor}->{'Name'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . " Dewpoint: " . sprintf("%.2f",$DewValue) . $DewUnit;
               if ($dewpoint_status eq 0) { push(@OK,$msg_txt) }
               if ($dewpoint_status eq 1) { push(@WARNING,$msg_txt) }
               if ($dewpoint_status eq 2) { push(@CRITICAL,$msg_txt) }
            }

            if (defined($dewdelta)) {
               # Dewpoint temperature should technically always be less than or equal to
               # drybulb temperature so it should always be safe to pass $TempValue-$DewValue
               # directly get_status();
               $dewpoint_status = $dewpoint_threshold->get_status($TempValue-$DewValue);
               my $msg_txt = $sensor_data{$sensor}->{'Name'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . " Delta: ". sprintf("%.2f",($TempValue-$DewValue)) . $DewUnit;
               if ($dewpoint_status eq 0) { push(@OK,$msg_txt) }
               if ($dewpoint_status eq 1) { push(@WARNING,$msg_txt) }
               if ($dewpoint_status eq 2) { push(@CRITICAL,$msg_txt) }
            }

         }
      }
   }
}


sub sanitize_infeed_set_thresholds() {

   if ($infeed =~ /,/) {
      @infeed_queries = split(',', $infeed);
   }
   # Only a single query
   else {
      push(@infeed_queries, $infeed);
   }

   # Validate metric input based on what we know today. This does NOT mean the CDU supports it.
   # We determine that later. This list is used elsewhere in the code, so it needs to be changed
   # in both places, sorry.
   # We also add the special "LoadImbalance" and "VoltageImbalance" queries here too, which are 
   # computed by this plugin internally, not derived directly from the CDU itself.
   foreach my $query (@infeed_queries) {
      if (! ($query =~ /\bLoadValue\b|\bCapacityUsed\b|\bPhaseCurrent\b|\bPhaseVoltage\b|\bPower\b|\bCrestFactor\b|\bPowerFactor\b|\bApparentPower\b|\bVoltage\b|\bEnergy\b|\bLoadImbalance\b|\bVoltageImbalance\b/) && !($query eq'')) {
         &exit_unknown("UNKNOWN - Infeed metric '$query' is not currently supported by this plugin.");
      }
   }

   # If there are queries but no warning or critical thresholds, that is a problem. We don't do any
   # automatic thresholds for infeed stats.
   if ( !($infeed_queries[0] eq '') && (!defined($warning_string)) && (!defined($critical_string))) {
      &exit_unknown("UNKNOWN - A threshold is required.");
   }

   if (defined($warning_string)) {
      # There is a warning passed to the command line, and it is a multi-part warning.
      if ($warning_string =~ /,/) {
         @warn_strings = split(',', $warning_string);
      
         # 3 or more sets of threholds were passed, and this is not legal.
         if (scalar(@warn_strings) ne scalar(@infeed_queries)) {
            &exit_unknown("UNKNOWN - Incorrect number of warning thresholds specified on command line");
         }
      }
      

      # There is a warning specified, but it's single dimensional.
      elsif (scalar(@infeed_queries) > 1) {
         &exit_unknown("UNKNOWN - single threshold is ambiguous when querying multiple tower metrics");
      }
      # I need an array of warnings even if it's only one element long
      else {
         push(@warn_strings, $warning_string);
      }
   }

   # Do the same for critical now

   if (defined($critical_string)) {
      # There is a warning passed to the command line, and it is a multi-part warning.
      if ($critical_string =~ /,/) {
         @crit_strings = split(',', $critical_string);

         # 3 or more sets of threholds were passed, and this is not legal.
         if (scalar(@crit_strings) ne scalar(@infeed_queries)) {
            &exit_unknown("UNKNOWN - Incorrect number of critical thresholds specified on command line");
         }
      }

      # There is a warning specified, but it's single dimensional.
      elsif (scalar(@infeed_queries) > 1) {
         &exit_unknown("UNKNOWN - single threshold is ambiguous when querying multiple tower metrics");
      }
      # I need an array of criticals even if it's only one element long
      else {
         push(@crit_strings, $critical_string);
      }
   }

   # Set all the thresholds. We use an array of objects to dynamically create the necessary
   # Thresholds for each metric we are tracking. The array indices between both @infeed_queries
   # and @threshold_array will always match up.
   for (my $i=0; $i < scalar(@infeed_queries); $i++) {

      # Both a WARNING and CRITICAL are defined
      if (@warn_strings && @crit_strings) {
         push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn_strings[$i], critical => $crit_strings[$i]));
      }

      # Just a CRITICAL is defined
      elsif (@crit_strings) {
         push(@threshold_array, Nagios::Plugin::Threshold->new(critical => $crit_strings[$i]));
      }

      # Just a WARNING is defined
      elsif (@warn_strings) {
         push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn_strings[$i]));
      }
   }
}




sub sanitize_tower_set_thresholds() {

   if ($tower =~ /,/) {
      @tower_queries = split(',', $tower);
   }
   # Only a single query
   else {
      push(@tower_queries, $tower);
   }

   # Validate metric input based on what we know today. This does NOT mean the CDU supports it. 
   # We determine that later. This list is used elsewhere in the code, so it needs to be changed
   # in both places, sorry.
   foreach my $query (@tower_queries) {
      if (! ($query =~ /VACapacity|VACapacityUsed|ActivePower|ApparentPower|PowerFactor|Energy|LineFrequency/) && !($query eq'')) {
         &exit_unknown("UNKNOWN - Tower metric '$query' is not currently supported by this plugin.");
      }
   }

   # If there are queries but no warning or critical thresholds, that is a problem. We don't do any
   # automatic thresholds for tower stats.
   if ( !($tower_queries[0] eq '') && (!defined($warning_string)) && (!defined($critical_string))) {
      &exit_unknown("UNKNOWN - A threshold is required.");
   }

   if (defined($warning_string)) {
      # There is a warning passed to the command line, and it is a multi-part warning.
      if ($warning_string =~ /,/) {
         @warn_strings = split(',', $warning_string);

         # 3 or more sets of threholds were passed, and this is not legal.
         if (scalar(@warn_strings) ne scalar(@tower_queries)) {
            &exit_unknown("UNKNOWN - Incorrect number of warning thresholds specified on command line");
         }
      }

      # There is a warning specified, but it's single dimensional.
      elsif (scalar(@tower_queries) > 1) {
         &exit_unknown("UNKNOWN - single threshold is ambiguous when querying multiple tower metrics");
      }
      # I need an array of warnings even if it's only one element long
      else {
         push(@warn_strings, $warning_string);
      }
   }

# Do the same for critical now

   if (defined($critical_string)) {
      # There is a warning passed to the command line, and it is a multi-part warning.
      if ($critical_string =~ /,/) {
         @crit_strings = split(',', $critical_string);

         # 3 or more sets of threholds were passed, and this is not legal.
         if (scalar(@crit_strings) ne scalar(@tower_queries)) {
            &exit_unknown("UNKNOWN - Incorrect number of critical thresholds specified on command line");
         }
      }

      # There is a warning specified, but it's single dimensional.
      elsif (scalar(@tower_queries) > 1) {
         &exit_unknown("UNKNOWN - single threshold is ambiguous when querying multiple tower metrics");
      }
      # I need an array of criticals even if it's only one element long
      else {
         push(@crit_strings, $critical_string);
      }
   }


   # Set all the thresholds. We use an array of objects to dynamically create the necessary
   # Thresholds for each metric we are tracking. The array indices between both @tower_queries
   # and @threshold_array will always match up.
   for (my $i=0; $i < scalar(@tower_queries); $i++) {

      # Both a WARNING and CRITICAL are defined
      if (@warn_strings && @crit_strings) {
         push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn_strings[$i], critical => $crit_strings[$i]));
      }

      # Just a CRITICAL is defined
      elsif (@crit_strings) {
         push(@threshold_array, Nagios::Plugin::Threshold->new(critical => $crit_strings[$i]));
      }

      # Just a WARNING is defined
      elsif (@warn_strings) {
         push(@threshold_array, Nagios::Plugin::Threshold->new(warning => $warn_strings[$i]));
      }
   }
}


# Pull apart multi-part thresholds, account for single-part thresholds, do basic input checking,
# set various thresholds using the Nagios::Plugin::Threshold class. Everything is in the global
# context, nothing is passed in or returned to this function.
sub sanitize_set_user_thresholds() {

   # Keep track of the number of metrics being queried. 
   my $num_metrics = 0;
   $num_metrics++ if (defined($temp));
   $num_metrics++ if (defined($humid));
   $num_metrics++ if (defined($dewtemp));
   $num_metrics++ if (defined($dewdelta));
   

   if (defined($warning_string)) {
      # There is a warning passed to the command line, and it is a multi-part warning.
      if ($warning_string =~ /,/) {
         if ($num_metrics < 2) {
            &exit_unknown("UNKNOWN - multi-part threshold is ambiguous if only querying a single sensor metric");
         }
         @warn_strings = split(',', $warning_string);

         # 3 or more sets of threholds were passed, and this is not legal.
         if (scalar(@warn_strings) > $num_metrics) {
            &exit_unknown("UNKNOWN - Too many warning thresholds specified on command line");
         }
      }

      # There is a warning specified, but it's single dimensional.
      elsif ($num_metrics > 1) {
         &exit_unknown("UNKNOWN - single warning threshold is ambiguous when querying multiple sensor metrics");
      }
   }


   # Now do the exact same thing for critical.
   if (defined($critical_string)) {
      # There is a critical passed to the command line, and it is a multi-part critical.
      if ($critical_string =~ /,/) {
         if ($num_metrics < 2) {
            &exit_unknown("UNKNOWN - multi-part threshold is ambiguous if only querying a single sensor metric");
         }
         @crit_strings = split(',', $critical_string);

         # 3 or more sets of threholds were passed, and this is not legal.
         if (scalar(@crit_strings) > $num_metrics) {
            &exit_unknown("UNKNOWN - Too many critical thresholds specified on command line");
         }
      }

      # There is a critical specified, but it's single dimensional.
      elsif ($num_metrics > 1) {
         &exit_unknown("UNKNOWN - single critical threshold is ambiguous when querying multiple sensor metrics");
      }
   }

   # Originally these assignments were done above, but you can apparently only call set_thresholds() once, 
   # subsequent calls do not "update" the thresholds as I had hoped, it re-creates it. So since this script
   # has a ton of flexibility built into it, we need to handle every case here:
   if (defined($temp) && (defined($critical_string) || defined($warning_string) ) ) {

      # Multi-part conditions 
      if (@crit_strings && @warn_strings) {
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warn_strings[0], critical => $crit_strings[0]);
      }
      elsif (@crit_strings) {
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds(critical => $crit_strings[0]);
      }
      elsif (@warn_strings) {
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warn_strings[0]);
      }

      # Single part, we are just checking temperature
      elsif (defined($critical_string) && defined($warning_string)) {
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warning_string, critical => $critical_string);
      }
      elsif (defined($critical_string)) {
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds(critical => $critical_string);
      }
      elsif (defined($warning_string)) {
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warning_string);
      }

      $user_temp_threshold_set = 1;
   }

   # Now do the same for humidity.
   if (defined($humid) && (defined($critical_string) || defined($warning_string) ) ) {

      my $humid_index;

      # The humidity threshold may be at array index 0 or 1, depending on if temperature and humidity are included, or not. 
      if (defined($temp) && defined($humid)) {
         $humid_index = 1;
      }
      else {
         $humid_index = 0;
      }

      # Multi-part conditions 
      if (@crit_strings && @warn_strings) {
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warn_strings[$humid_index], critical => $crit_strings[$humid_index]);
      }
      elsif (@crit_strings) {
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds(critical => $crit_strings[$humid_index]);
      }
      elsif (@warn_strings) {
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warn_strings[$humid_index]);
      }

      # Single part, we are just checking humidity.
      elsif (defined($critical_string) && defined($warning_string)) {
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warning_string, critical => $critical_string);
      }
      elsif (defined($critical_string)) {
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds(critical => $critical_string);
      }
      elsif (defined($warning_string)) {
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warning_string);
      }

      $user_humid_threshold_set = 1;
   } 

   # And do this for dewpoint. There are no built-in thresholds for it, so they need to be specified on the command line.
   if ((defined($dewtemp) || defined($dewdelta)) && (defined($critical_string) || defined($warning_string) ) ) {
    
      my $dew_index; 
      # the dewpoint threshold may be at array index 1 or 2, depending on if temperature and humidity are included, or not. 
      if (defined($temp) && defined($humid)) {
         $dew_index = 2;
      }
      else {
         $dew_index = 1;
      }
      # Multi-part conditions 
      if (@crit_strings && @warn_strings) {
         $dewpoint_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warn_strings[$dew_index], critical => $crit_strings[$dew_index]);
      }
      elsif (@crit_strings) {
         $dewpoint_threshold = Nagios::Plugin::Threshold->set_thresholds(critical => $crit_strings[$dew_index]);
      }
      elsif (@warn_strings) {
         $dewpoint_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warn_strings[$dew_index]);
      }

      # Single part, we are just checking humidity.
      elsif (defined($critical_string) && defined($warning_string)) {
         $dewpoint_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warning_string, critical => $critical_string);
      }
      elsif (defined($critical_string)) {
         $dewpoint_threshold = Nagios::Plugin::Threshold->set_thresholds(critical => $critical_string);
      }
      elsif (defined($warning_string)) {
         $dewpoint_threshold = Nagios::Plugin::Threshold->set_thresholds(warning => $warning_string);
      }

      $user_dewpoint_threshold_set = 1;
   } 
}


# Program termination. This function handles the various exit strategies based on what Nagios exit
# codes have been generated throughout the runtime. All needed objects are in the global scope, this
# function neither takes any arguments or returns anything. This should always be called last.
sub plugin_exit() {

   # I prefer to output both CRITICAL and WARNING if they both exist. This is non-standard, 
   # and if it seriously bothers you, feel free to "fix" it. Clearly, if multiple metrics
   # are monitored at once, it may be useful to know that one sensor is in a CRITICAL state
   # while another one is in a WARNING state.
   if (@CRITICAL && @WARNING) {
      print "CRITICAL - $systemLocation";
      foreach(@CRITICAL) {
         print ", $_";
      }
      print ", WARNING - ";
      foreach(@WARNING) {
         # Last element of the array test.
         if (\$_ == \$WARNING[-1]) {
            print "$_";
         }
         else { print "$_, " }
      }
      print "\n";
      exit(2);
   }

   if (@CRITICAL) {
      print "CRITICAL - $systemLocation";
      foreach(@CRITICAL) {
         print ", $_";
      }
      print "\n";
      exit(2);
   }

   if (@WARNING) {
      print "WARNING - $systemLocation";
      foreach(@WARNING) {
         print ", $_";
      }
      print "\n";
      exit(1);
   }

   # There are certain scenarios where we collect UNKNOWN states but don't exit on them.
   # One example is a Temp/Humid sensor error. I chose to make this UNKNOWN because I 
   # didn't want to get paged if a sensor failed or got unplugged.
   if (@UNKNOWN) {
      print "UNKNOWN - $systemLocation";
      foreach(@UNKNOWN) {
         print ", $_";
      }
      print "\n";
      exit(3);
   }

   if (@OK) {
      if (defined($oksummary)) {
         print "OK - $systemVersion, Location: $systemLocation, " . scalar(@OK) . " metrics are OK";
      }
      else {
         print "OK - $systemLocation";
         foreach(@OK) {
            print ", $_";
         }
      }
      print "\n";
      exit(0);
   }

   else {
      print "UNKNOWN - unhandled exception\n";
      exit(3);
   }

}



__END__

=head1 NAME

check_cdu - Check various metrics from a Server Technology Cabinet Distribution Unit (CDU)

=head1 VERSION

This documentation refers to check_cdu version 1.3

=head1 APPLICATION REQUIREMENTS

 Several standard Perl libraries are required for this program to function. Namely, Net::SNMP,
 Getopt::Std, Getopt::Long, Nagios::Plugin::Threshold

=head1 GENERAL USAGE

 check_cdu.pl -H <host> -C <community> [-t SNMP timeout] [-p SNMP port] <additional options>

=head1 REQUIRED ARGUMENTS

 Only the hostname and community are required. Timeout will default to 2 seconds, port 161.

=head1 THRESHOLDS

 I opted to use the Nagios::Plugin::Threshold class to handle thresholds. In general I do
 not prefer Nagios::Plugins objects, but I just simply could not avoid using the Threshold
 class. I apologize for the added dependency, I just could not afford re-inventing the wheel.
 The benefit is that the threshold logic used in this plugin follows the standard used in 
 many other plugins. For reference, here are the general threshold guidelines:

 Range definition	Generate an alert if x...
 10			< 0 or > 10, (outside the range of {0 .. 10})
 10:			< 10, (outside {10 .. ?})
 ~:10			> 10, (outside the range of {-? .. 10})
 10:20			< 10 or > 20, (outside the range of {10 .. 20})
 @10:20?		10 and ? 20, (inside the range of {10 .. 20})
 10			< 0 or > 10, (outside the range of {0 .. 10})

 Read: http://nagiosplug.sourceforge.net/developer-guidelines.html#THRESHOLDFORMAT
 For the full, official, documentation


=head1 FULL DOCUMENTATION

 check_cdu is intended to provide extremely flexible and extensive monitoring support for 
 Server Technology Cabinet Distribution Units (CDU). In general the workflow for this application
 follows this procedure:
 
 1. Pull in an entire SNMP table using a Net::SNMP session and get_table().
 2. Renumerate these "flat" values into a structured hash
 3. Evaluate any options or thresholds passed on the command line by the user.
 4. Process the command line options against the data collected from the CDU
 5. Exit appropriately given the status results

 This workflow is generally followed in four slightly different ways depending on the desired options.
 These four procedures are:

 1. General System
 2. Environment
 3. Towers
 4. Infeeds

=head2 Environment

 An optional feature of a CDU are temperature and humidity probes. On most units, only two T/H
 ports exist. When using an EMCU 1-1B in conjuction with a CDU this can be expanded to 4 T/H 
 ports. I am not aware of a way to increase the number of T/H probes past this amount on a single
 CDU. Regardless, this application is designed to support any number of T/H probes. 

 Prior to running any checks for temperature or humidity, this plugin will check the T/H probe 
 status. The following states will result in an UNKNOWN return:
 
 notFound
 readError
 lost
 noComm

 This applies to both temperature and humidity. The LowThresh and HighThresh states are ignored.
 Any of these states will issue an UNKNOWN return. Since there is no data available, it's not
 logical to initiate a WARNING or CRITICAL and roll someone out of bed. This behavior can easily
 be changed in the code, if desired.

 In its simplest form, the environment checks will query all available T/H probes connected to the
 system. The CDU has internal High/Low thresholds configured for both Temperature and Humidity, and 
 this is done on a per sensor basis. Without any arguments, this plugin will honor those values.
 Considering that there is only one high:low range, I opted to designate this as a WARNING threshold.
 This behavior can easily be changed in the code to the CRITICAL state, if desired, but it is NOT 
 modifiable from the command line. A basic invocation would resemble:

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --humid
 OK -BLDG_ROOM_RACK, Bottom-Rack-Inlet_F31(A1): 18C, Bottom-Rack-Inlet_F31(A1): 43%, \
 Bottom-Rack-Exhaust_F32(A2): 33C, Bottom-Rack-Exhaust_F32(A2): 16%, Top-Rack-Inlet_F31(B1): 24C, \
 Top-Rack-Inlet_F31(B1): 28%, Top-Rack-Exhaust_F32(B2): 36.5C, Top-Rack-Exhaust_F32(B2): 12%
 
 The plugin output always includes the systemLocation defined on the CDU first. The various objects 
 queried are then returned in a comma separated list. For temperature and humidity probes, the sensor 
 Name is returned along with the ID in parantheses. If names haven't been set, the defaults will still 
 be displayed. Finally the value is listed for each sensor. The temperature scale is automatically 
 determined from the TempScale object provided via SNMP. For instances where the CDU is configured 
 for one scale, but the user desires the plugin to report in another scale, the --fahrenheit and 
 --celsius options are quite handy:

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --humid --fahrenheit
 OK - BLDG_ROOM_RACK, Bottom-Rack-Inlet_F31(A1): 62.6F, Bottom-Rack-Inlet_F31(A1): 45%, Bottom-Rack-Exhaust_F32(A2): 91.4F 

 --celsius works in a similar fashion. If a scale is passed to the plugin and the T/H probe is already
 configured for that scale, no error will occur. The values will be reported in the native scale for 
 that sensor.

 Expanding on this basic functionality is the --ths option. --ths allows the user to select 
 which sensors to query, based on the sensor ID (not the name!). --ths will automatically determine
 if the sensors exist, and exit UNKNOWN if they were not found. All of the regular sensor status
 checks are still performed. 

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --ths A1,B2 --fahrenheit
 OK - BLDG_ROOM_RACK, Bottom-Rack-Inlet_F31(A1): 62.6F, Top-Rack-Exhaust_F32(B2): 97.7F 

 Note I also left out the --humid option. Either option can be specified alone, or both together, 
 providing maximum flexibility for designing purpose-built nagios service checks. 

 User supplied WARNING and CRITICAL thresholds can be applied to the temperature and humidity
 sensors using the --warning and --critical directives. This overrides the automatic threshold
 logic that relies upon the internal CDU configuration. Either --warning or --critical can be used,
 or both can be used together. When querying multiple temperature sensors, a single threshold is 
 applied across all sensors. The same is true for querying multiple humidity sensors. Both temperature
 and humidity can be queried together in the same command, by "chaining" the thresholds together.
 Here are a couple examples:

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --fahrenheit --ths A1,B1 --warning 60:80
 OK - BLDG_ROOM_RACK, Bottom-Rack-Inlet_F31(A1): 62.6F, Top-Rack-Inlet_F31(B1): 77F

 (Query just the temperature from T/H probes A1 and B1 and apply a warning threshold to alarm if
 either sensor falls below 60F or above 80F)

 $ check_cdu.pl -H 192.168.0.1 -C public --humid --ths A2,B2 --warning 10:70
 OK - BLDG_ROOM_RACK, Bottom-Rack-Exhaust_F32(A2): 18%, Top-Rack-Exhaust_F32(B2): 13%

 (Query just the humidity from T/H probes A2 and B2 and apply a warning threshold to alarm if 
 either sensor falls below 10% or above 70% relative humidity)

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --humid --fahrenheit --ths A1 --warning 80,20: --critical 95,10:
 OK - BLDG_ROOM_RACK, Bottom-Rack-Inlet_F31(A1): 64.4F, Bottom-Rack-Inlet_F31(A1): 48%

 (Check just sensor A1, but query both temperature and humidity from this sensor. If the temperature
 rises above 80F or the humidity falls below 20% generate a WARNING. If the temperature rises above
 95 or the humidity falls below 10% generate a CRITICAL.)

 IMPORTANT NOTE: When specifying both --temp and --humid the thresholds are chained together as
 temperature_threshold,humidity_threshold regardless of which order --temp and --humid are passed!!
 aka the following are equivalent:
 '--temp --humid --warning 45,60' , '--humid --temp --warning 45,60'
 The following are NOT equivalent:
 '--temp --humid --warning 45,60', '--humid --temp --warning 60,45'

 Starting in version 1.3 monitoring dewpoint temperature and dewpoint delta is supported. The
 CDU does not natively support dewpoint, but it can be calculated given temperature and humidity.
 Dewpoint is calculated using constants from J Applied Meteorology and Climatology and the
 dewpoint calculations provided at: http://en.wikipedia.org/wiki/Dew_point#Calculating_the_dew_point
 There are two ways to monitor dewpoint. First is with the "--dewtemp" option. This simply 
 calculates the air temperature dewpoint of any given sensor and applies the user supplied
 thresholds to the value. Using the "--dewdelta" directive calculates the differential temperature
 between the air temperature and calculated air temperature dewpoint values. This is especially
 useful for determining how close a sensor is to reaching the dewpoint temperature, and hence
 when condesnsation might start forming within a data center. An example invocation would look like:

 $ check_cdu.pl -H 192.168.0.1 -C public --dewdelta --fahrenheit --ths A1,C1 --warning 10: --critical 5:
 OK - BLDG_ROOM_RACK, Bottom-Rack-Inlet(A1) Delta: 39.00F, Top-Rack-Inlet(C1) Delta: 40.37F

 This check would initiate a WARNING if the dewpoint is 10F or less from the air temperature and a 
 CRITICAL if the dewpoint is 5F or less from the air temperature. I believe this would be a typical
 use for this function. The dewpoint temperature can never be greater than air temperature, only
 less than, or equal to.

 Since the CDU does not have built-in thresholds for dewpoint, it is required to use either 
 --warning or --critical in conjunction with either --dewtemp or --dewdelta. Like --temp and
 --humid options chaining is supported with the dewpoint options. The order of the chained
 thresholds is always temp,humidity,dewpoint. You cannot specify --dewdelta and --dewtemp in
 the same invocation. An complex example invocation would be:

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --humid --dewdelta --fahrenheit --ths A1,C1 \
   --warning 80,50,10: --critical 90,80,5: 
 OK - BLDG_ROOM_RACK, Bottom-Rack-Inlet(A1): 67.1F, Bottom-Rack-Inlet(A1): 24%, Bottom-Rack-Inlet(A1) \
 Delta: 37.96F, Top-Rack-Inlet(C1): 68F, Top-Rack-Inlet(C1): 22%, Top-Rack-Inlet(C1) Delta: 40.22F

 This command checks two sensors for temperature, humidity and dewpoint delta. Temperature 
 WARNING above 80, CRITICAL above 90. Humidity WARNING above 50, CRITICAL above 80. Dewpoint Delta
 WARNING if less than 10 and CRITICAL if less than 5.

=head2 Towers

 Tower state and statistics are checked using the --tower directive. If specified with no arguments
 only the overall state of the tower(s) are checked. The ability to query a specific tower does not
 exist at this time. If the 'noComm' state is encountered for a tower a WARNING state is generated.
 This is likely only possible on a slave tower. If the master tower is in state 'noComm', I doubt you'd
 get this far with it ;) If 'fanFail', 'overTemp' or 'nvmFail' states are encountered, the state is 
 returned as CRITICAL. The 'outOfBalance' state returns WARNING.

 Various metrics from the tower can also be queried by passing them to the --tower directive as a 
 comma separated list. At the time of development, these metrics are only supported on PIPS units.
 A regular SMART or SWITCHED CDU will likely not benefit from any of these enhancements. The plugin
 will correctly identify the absence of these metrics if you attempt to query them. The metrics are:

 VACapacity
 ApparentPower
 VACapacityUsed
 ActivePower
 Energy
 LineFrequency

 It is very important to note that the 'Status' checks are largely skipped when querying any of these
 metrics. The 'fanFail' and 'overTemp' states are completely ignored. If the 'noComm' state is 
 encountered, the metric(s) are skipped and a state UNKNOWN is returned. Given this, to fully utilize
 the features of this plugin one should ALWAYS have a service check using just '--tower'. It was not
 logical to exit on WARNING/CRITICAL for a 'noComm' state multiple times (say, for instance if there
 are separate service checks defined for every metric listed above).

 The towers are identified similar to the T/H probes, in the form of NAME(ID): VALUE. These are all
 configurable on the CDU itself. Typically, a circuit name would be used for a Tower name. Thresholds
 are applied in a similar manner to the --temp and --humid checks. ORDER DOES MATTER. The order in 
 which the metrics are listed is the order in which the thresholds should be "chained". The same logic
 applies to these thresholds, see the THRESHOLDS section for specifics.

 Here are some examples:

 $ check_cdu.pl -H 192.168.0.1 -C public --tower
 OK - BLDG_ROOM_RACK, TowerA(A) Status: normal(0), TowerB(B) Status: normal(0)a

 $ check_cdu.pl -H 192.168.0.1 -C public --tower ApparentPower,ActivePower,VACapacityUsed --warning 1200,1000,30
 OK - BLDG_ROOM_RACK, TowerA(A) ApparentPower: 993VA, TowerA(A) ActivePower: 939W, TowerA(A) VACapacityUsed: 9.1%, \
 TowerB(B) ApparentPower: 927VA, TowerB(B) ActivePower: 870W, TowerB(B) VACapacityUsed: 8.5%

 (Check that ApparentPower does not exceed 1200VA, ActivePower does not exceed 1000W and the Capacity
 used does not exceed 30%. If any of these scenarios occur, generate a WARNING)

 $ check_cdu.pl -H 192.168.0.1 -C public --tower Energy --warning 10000 --critical 15000
 OK - BLDG_ROOM_RACK, TowerA(A) Energy: 6654kWh, TowerB(B) Energy: 7658kWh

 (If the kWh consumption of either tower exceeds 10,000 generate a WARNING. If it exceeds 15,000 
 generate a CRITICAL. Say you're in a co-lo paying for power utilization and your piggy bank will
 run dry if you use too much power ...)

 $ check_cdu.pl -H 192.168.0.1 -C public --tower VACapacity --warning 10800 
 WARNING - BLDG_ROOM_RACK, TowerA(A) VACapacity: -1VA

 (This is a very bizarre but interesting scenario. I included VACapacity because it was there, but
 who would logically check a static value such as the capacity of a tower? Well, it turns out that
 this particular unit is slightly broken and the Capacity is -1. This should just provide some ideas
 on why it may be useful to monitor things that otherwise wouldn't make sense)

=head2 Infeeds

 Infeed state and statistics are checked using the --infeed directive. It is very similar to the --tower
 check. If specified with no agruments, the infeed 'Status' and 'LoadStatus' objects are checked. The 
 ability to query a specific infeed does not exist at this time (and likely never will). The following
 infeed Statuses will generate a WARNING:

 noComm
 offWait
 onWait
 off 
 reading

 A CRITICAL will be generated if the Infeed has the following Status:

 offError
 onError
 offFuse
 onFuse

 Likewise the LoadStatus object is checked for each infeed as well. A WARNING is generated for the 
 following LoadStatus conditions:

 noComm
 reading
 loadLow

 I wasn't sure what the 'reading' state was, this state is also present across many other CDU 
 objects. There is a good chance this state simply infers that the state is currently being
 "read" or updated, and it's likely that this state will be ignored in future versions of the 
 plugin if that is the case. The loadLow must be determined by an internal CDU threshold, however
 this threshold isn't available via SNMP - so I left it alone. A CRITICAL is generated for the 
 other LoadStatus states:

 notOn
 loadHigh
 overLoad
 readError

 Simple modifications to the code can be done to move these various Statuses between the CRITICAL
 and WARNING states if desired, but it is not possible from the command line.

 Similar to the --tower directive, many of these Status checks are skipped when querying specifc
 metrics from the infeed. If any metrics are provided to --infeed, the infeed Status is checked 
 for the 'noComm' status. If this is true, the plugin will append this to the UNKNOWN 'bucket'
 and skip checking the metric. The following infeed metrics are currently supported:

 PhaseVoltage *
 Voltage
 CapacityUsed *
 Power
 ApparentPower *
 Energy *
 LoadValue
 PhaseCurrent *
 CrestFactor *
 PowerFactor * 

 * These metrics are only available on PIPS units.

 The infeeds are identified similar to the T/H probes, in the form of NAME(ID): VALUE. These are all
 configurable on the CDU itself. Typically, a circuit name would be used for an infeed name. Thresholds
 are applied in a similar manner to the --temp and --humid checks. ORDER DOES MATTER. The order in 
 which the metrics are listed is the order in which the thresholds should be "chained". The same logic
 applies to these thresholds, see the THRESHOLDS section for specifics.

 A special note on PowerFactor: An unloaded infeed will typically report -0.01 for the Power Factor. 
 It does not seem logical to apply the provided threshold to this value. So if the Power Factor is 
 less than 0 the threshold is not used and the state is simply assumed to be 'OK'. 

 Some examples:

 $ check_cdu.pl -H 192.168.0.1 -C public --infeed
 OK - BLDG_ROOM_RACK, TowerA_InfeedA(AA) Status: on(1), TowerA_InfeedA(AA) LoadStatus: normal(0), \
 TowerA_InfeedB(AB) Status: on(1), TowerA_InfeedB(AB) LoadStatus: normal(0), TowerA_InfeedC(AC) Status: \
 on(1), TowerA_InfeedC(AC) LoadStatus: normal(0), TowerB_InfeedA(BA) Status: on(1), TowerB_InfeedA(BA) \
 LoadStatus: normal(0), TowerB_InfeedB(BB) Status: on(1), TowerB_InfeedB(BB) LoadStatus: normal(0), \
 TowerB_InfeedC(BC) Status: on(1), TowerB_InfeedC(BC) LoadStatus: normal(0)

 (This is a basic tower check for a master/slave 3 phase CDU. There are 6 infeeds total across both 
 towers, and two separate checks are performed (Status,LoadStatus) for each infeed. This is a lot of data)

 $ check_cdu.pl -H 192.168.0.1 -C public --infeed LoadValue --warning 12 --critical 24
 OK - BLDG_ROOM_RACK, TowerA_InfeedA(AA) LoadValue: 4.07A, TowerA_InfeedB(AB) LoadValue: 3.21A, \
 TowerA_InfeedC(AC) LoadValue: 1.62A, TowerB_InfeedA(BA) LoadValue: 3.61A, TowerB_InfeedB(BB) LoadValue: \
 2.76A, TowerB_InfeedC(BC) LoadValue: 1.73A

 (This is a simple load/current check which applies a warning and critical threshold to the load of all 6
 infeeds on a dual tower 3 phase CDU.)

 $ check_cdu.pl -H 192.168.0.1 -C public --infeed ApparentPower,CapacityUsed --warning 1000,20
 OK - BLDG_ROOM_RACK, TowerA_InfeedA(AA) ApparentPower: 673VA, TowerA_InfeedA(AA) CapacityUsed: 12.6%, \
 TowerA_InfeedB(AB) ApparentPower: 0VA, TowerA_InfeedB(AB) CapacityUsed: 10.5%, TowerA_InfeedC(AC) \
 ApparentPower: 317VA, TowerA_InfeedC(AC) CapacityUsed: 5.3%, TowerB_InfeedA(BA) ApparentPower: 575VA, \
 TowerB_InfeedA(BA) CapacityUsed: 12%, TowerB_InfeedB(BB) ApparentPower: 0VA, TowerB_InfeedB(BB) CapacityUsed: \
 8.9%, TowerB_InfeedC(BC) ApparentPower: 348VA, TowerB_InfeedC(BC) CapacityUsed: 5.7%
 
 (Generate a warning if the ApparentPower of any infeed exceeds 1000VA, and generate a warning if the 
 Capacity Used exceeds 20% on any infeed)

 PhaseVoltage and PhaseCurrent use the PhaseID instead of infeedID in the plugin output. Throughout our
 testing, it has been difficult to ascertain a difference between PhaseVoltage and Voltage. There is 
 generally a considerable difference between PhaseCurrent and LoadValue, however it most likely makes
 sense to only check one of these. 

=head2 Enhanced Infeed checks

 There are two additional metrics that can be checked with the '--infeed' directive. They are:

 LoadImbalance
 VoltageImbalance

 These metrics are not provided directly by the CDU, rather they are computed internally by the plugin.
 Please note, these special metrics are ONLY available on 3 phase units. Some versions of the CDU 
 firmware provide a '3-Phase Load Out-of-Balance Threshold' setting and the results are displayed on
 the 'istat' menu. None of this information is provided via SNMP. Thresholds are required for either
 of these computed metrics. Unlike the display in 'istat' only the out-of-balance infeed(s) will be 
 displayed, not infeeds across the entire tower. I used a basic 3 phase motor load phase imbalance
 equation to generate the imbalance percentages for both Current and Voltage:

 Percent imbalance = maximum deviation from average / average of three phases * 100

 When an infeed is queried for either voltage or current imbalance, the plugin determines which tower 
 the infeed is a part of. All infeed values (voltage or current) for that tower are then averaged 
 together. The deviation from the average is then determined for this particular infeed, accomodating
 either a negative or positive delta from the average. This is then divided by the average and 
 multiplied by 100 to determine the percent imbalance. This equation was pulled from the following
 document:

 http://support.fluke.com/educators/download/asset/2161031_b_w.pdf

 An example invocation of this check would look like:

 $ check_cdu.pl -H 192.168.0.1 -C public --infeed LoadImbalance --warning 20 --critical 30
 CRITICAL - BLDG_ROOM_RACK, TowerA_InfeedA(AA) LoadImbalance: 39.07%, TowerA_InfeedC(AC) \
 LoadImbalance: 50.46%, TowerB_InfeedA(BA) LoadImbalance: 33.54%, TowerB_InfeedC(BC) LoadImbalance: 34.54%

 (Generate a WARNING if the load imbalance of any infeed exceeds 20%, and a CRITICAL if the imbalance
 exceeds 30%. Clearly, this is not a well balanced rack! Hence the need for such a check)

 The same can be done for voltage, however the margins should be much, much smaller than load.
 This can be useful to detect bad incoming power conditions. Unfortunately this only evaluates 
 an imbalance across the phases of a single tower. A more useful approach would be to judge 
 imbalance between two separate towers, and hence two separate feeds/circuits which could be 
 coming from two separate sources (ie. UPS/utility). Currently that functionality does not exist.
 Here is an example:

 $ check_cdu.pl -H 192.168.0.1 -C public --infeed VoltageImbalance --warning .5 --critical 2
 WARNING - BLDG_ROOM_RACK, TowerB_InfeedB(BB) VoltageImbalance: 0.65%

 (Generate a WARNING if the imbalance between voltages per infeed is greater than .5% and a
 CRITICAL if the imbalance is greater than 2%)

=head2 Plugin Termination
 
 Numerous scenarios exist where the plugin will exit abnormally. This could be due to user input error,
 or failure to retrieve required SNMP data, etc. In all identifiable cases, the plugin will exit with an
 UNKNOWN state and a descriptive message indicating the failure. Users should be aware that if all SNMP
 calls fail, monitoring of the CDU may be effectively rendered useless if UNKNOWN states are not reported
 (this is common). This is dissimilar to plugins like check_nrpe that exit CRITICAL if an SSL negotiation
 erorr occurs! 

 Throughout the workflow of the plugin metrics are evaluated against thresholds and the results are placed
 into various 'buckets' reflecting OK,WARNING,CRITICAL and UNKNOWN states. At the end of the workflow, 
 reporting is done based upon the presence or absence of these buckets. If both CRITICAL and WARNING 
 conditions exist, they are BOTH reported in the plugin_output text, however the state is reported as
 CRITICAL. An example of this can be seen in the following output:

 $ check_cdu.pl -H 192.168.0.1 -C public --temp --humid --ths A1 --warning 16,30 --critical 20,40
 CRITICAL - BLDG_ROOM_RACK, Bottom-Rack-Inlet_F31(A1): 43%, WARNING - Bottom-Rack-Inlet_F31(A1): 17C

 Some options end up producing a large amount of output, and this could easily exceed what Nagios can
 accept, or also exceed character limits on various notification devices (maybe you're tweeting your
 CDU status for instance ;P) The '--oksummary' option exists to summarize the output for any type of 
 check being done. If all metrics being checked are in state 'OK' the output supresses the specifics
 of these metrics and simply reports 'N metrics are OK' The version and location are also displayed
 in the plugin_output.

 

=head1 INCOMPATIBILITIES

 None. See Bugs.

=head1 BUGS AND LIMITATIONS

 None.
 If you experience any problems please contact me. (eric.schoeller <at> coloradoDOTedu)

=head1 AUTHOR

Eric Schoeller (eric.schoeller <at> coloradoDOTedu)

=head1 LICENCE AND COPYRIGHT

 Copyright (c) 2013 Eric Schoeller (eric.schoeller <at> coloradoDOTedu).
 All rights reserved.

 This module is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License.
 See L<http://www.fsf.org/licensing/licenses/gpl.html>.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

