#!/usr/bin/perl -w

# check_cdu.pl Written by Eric Schoeller for the University of Colorado Boulder - 20130121
my $VERSION="2.3";
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.
# 20161023: Version 2.0 introduced support for Sentry4 products (finally)
# 20161024: Added functionality to monitor dry contacts provided by an EMCU-1-1B
# 20161130: Minor bug fix, needed to use @st4_global_device_status for translating contact closure status.
# 20170718: Added a cheap hack to disable load imbalance reporting for lightly loaded units. 
#	    Added "unknown" to the list of acceptable states for Phase Reactance on Sentry4 products
# 20180312: Ignore Phase PowerFactor readings of "-0.01" on Sentry4 units, set to OK status regardless of threshold. 

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

# This is the load imbalance ignore threshold. For an infeed with an average current draw of less than
# this value the imbalance deviation will automatically be assumed to be 0.00%. It is impossible to 
# manage phase balancing on infeeds with extremely low load due to having only a couple devices powered
# up. This is a cheap hack and sadly I didn't have time to come up with something better. 
my $IGNORE_LOAD_IMBAL_THRESHOLD = 1;

# 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 ($sentryVersion, $systemVersion, $systemLocation, $systemTotalPower, $systemPowerFactor);
my ($st4SystemProductName, $st4SystemFirmwareVersion, $st4TempSensorScale);
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 ($cord, @cords, @cord_queries, $line, @lines, @line_queries, $phase, @phases, @phase_queries, $branch, @branches, @branch_queries);
my (%sensor_data, %tower_data, %infeed_data, %cord_data, %line_data, %phase_data, %branch_data);
my ($contact, @contacts, @contacts_list, %contact_data);

# Sentry4 Products now use a global device status to cover all components. This is lifted directly from the MIB file.
# INTEGER 3,4 had no definition at the time this application was written, and subsequently are called 'st4Undefined'.
my @st4_global_device_status = qw(normal disabled purged st4Undefined st4Undefined reading settle notFound lost readError noComm pwrError breakerTripped fuseBlown lowAlarm lowWarning highWarning highAlarm alarm underLimit overLimit nvmFail profileError conflict);

my @st4_global_device_state = qw(unknown on off);

#
# 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,
      "cord:s"		=>	\$cord,
      "line:s"		=>	\$line,
      "phase:s"		=>	\$phase,
      "branch:s"	=>	\$branch,
      "ths=s"		=>	\$ths,
      "contact=s"	=>	\$contact,
      "version"		=>	\$version
      ) or &exit_unknown("UNKNOWN - unsupported option specified on command line"); 

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;
   if ($sentryVersion eq 3) {
      &build_sensor_data;
      &process_sensor_data;
   }
   elsif ($sentryVersion eq 4) {
      &st4_build_sensor_data;
      &st4_process_sensor_data;
   }
}

if (defined($contact)) {
 
   if (defined($contact)) {
      if ($contact =~ /,/) {
         @contacts_list = split(',', $contact);
      }
      else {
         push(@contacts_list, $contact);
      }
   
      foreach my $contact (@contacts_list) {
         if (! ($contact =~ /\b[1-9]\b/)) {
            &exit_unknown("UNKNOWN - Basic sensor ID verification failed for '$contact'. If this an error, read the docs, change the regex");
         }
      }
   }

   if ($sentryVersion eq 4) {
      &build_sentry4_contact_data;
   }
   elsif ($sentryVersion eq 3) {
      &build_sentry3_contact_data;
   }
   else {
      &exit_unknown("UNKNOWN - dry contact monitoring not available on this Sentry Product");
   }

   &process_contact_data;

}

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

   if ($sentryVersion eq 4) {
      &exit_unknown("UNKNOWN - 'tower' metrics are unavailable on Sentry4 products");
   }

   &build_tower_data;
   &sanitize_tower_set_thresholds;
   &process_tower_data;


}

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

   if ($sentryVersion eq 4) {
      &exit_unknown("UNKNOWN - 'infeed' metrics are unavailable on Sentry4 products");
   }

   &build_infeed_data;
   &sanitize_infeed_set_thresholds;
   &process_infeed_data;
}

### Sentry4 Cord
if (defined($cord)) {

   if ($sentryVersion eq 3) {
      &exit_unknown("UNKNOWN - 'cord' metrics are unavailable on Sentry3 products");
   }

   &build_cord_data;

   # Had to pull this bit of code out of sanitize_cord_set_thresholds, since we need it
   # for situations with and without thresholds set.
   # Basically build an array of all the various cord metrics we intend to query.
   if ($cord =~ /,/) {
      @cord_queries = split(',', $cord);
   }
   # Only a single query
   else {
      push(@cord_queries, $cord);
   }

   # Only run sanitize function if we pass thresholds on the command line. 
   if ( (defined($warning_string)) || (defined($critical_string)) ) {
      &sanitize_cord_set_thresholds;
   }
   &process_cord_data;
}

### Sentry4 Line 
if (defined($line)) {

   if ($sentryVersion eq 3) {
      &exit_unknown("UNKNOWN - 'line' metrics are unavailable on Sentry3 products");
   }

   &build_line_data;

   # Had to pull this bit of code out of sanitize_cord_set_thresholds, since we need it
   # for situations with and without thresholds set.
   # Basically build an array of all the various cord metrics we intend to query.
   if ($line =~ /,/) {
      @line_queries = split(',', $line);
   }
   # Only a single query
   else {
      push(@line_queries, $line);
   }

   # Only run sanitize function if we pass thresholds on the command line. 
   if ( (defined($warning_string)) || (defined($critical_string)) ) {
      &sanitize_line_set_thresholds;
   }
   &process_line_data;
}

### Sentry4 Phase 
if (defined($phase)) {

   if ($sentryVersion eq 3) {
      &exit_unknown("UNKNOWN - 'phase' metrics are unavailable on Sentry3 products");
   }

   &build_phase_data;

   # Had to pull this bit of code out of sanitize_cord_set_thresholds, since we need it
   # for situations with and without thresholds set.
   # Basically build an array of all the various cord metrics we intend to query.
   if ($phase =~ /,/) {
      @phase_queries = split(',', $phase);
   }
   # Only a single query
   else {
      push(@phase_queries, $phase);
   }

   # Only run sanitize function if we pass thresholds on the command line. 
   if ( (defined($warning_string)) || (defined($critical_string)) ) {
      &sanitize_phase_set_thresholds;
   }
   &process_phase_data;
}

### Sentry4 Branch
if (defined($branch)) {

   if ($sentryVersion eq 3) {
      &exit_unknown("UNKNOWN - 'branch' metrics are unavailable on Sentry3 products");
   }

   &build_branch_data;

   # Had to pull this bit of code out of sanitize_branch_set_thresholds, since we need it
   # for situations with and without thresholds set.
   # Basically build an array of all the various cord metrics we intend to query.
   if ($branch =~ /,/) {
      @branch_queries = split(',', $branch);
   }
   # Only a single query
   else {
      push(@branch_queries, $branch);
   }

   # Only run sanitize function if we pass thresholds on the command line. 
   if ( (defined($warning_string)) || (defined($critical_string)) ) {
      &sanitize_branch_set_thresholds;
   }
   &process_branch_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--cord\n";
   print "\t   With no arguments, simply checks infeed 'Status' and 'State'. Can also query other metrics.\n";
   print "\t   The following status metrics are currently available:\n";
   print "\t   State,Status,ActivePowerStatus,ApparentPowerStatus,PowerFactorStatus,OutOfBalanceStatus\n";
   print "\t   The following metered metrics are currently available:\n";
   print "\t   PowerCapacity,ActivePower,ApparentPower,PowerUtilized,PowerFactor,Energy,Frequency,OutOfBalance\n\n";

   print "\t--line\n";
   print "\t   With no arguments, simply checks infeed 'Status' and 'State'. Can also query other metrics.\n";
   print "\t   The following status metrics are currently available:\n";
   print "\t   State,Status,CurrentStatus\n";
   print "\t   The following metered metrics are currently available:\n";
   print "\t   CurrentCapacity,Current,CurrentUtilized\n\n";

   print "\t--phase\n";
   print "\t   With no arguments, simply checks infeed 'Status' and 'State'. Can also query other metrics.\n";
   print "\t   The following status metrics are currently available:\n";
   print "\t   State,Status,VoltageStatus,PowerFactorStatus,Reactance\n";
   print "\t   The following metered metrics are currently available:\n";
   print "\t   Voltage,VoltageDeviation,Current,CrestFactor,ActivePower,ApparentPower,PowerFactor,Energy\n\n";

   print "\t--branch\n";
   print "\t   With no arguments, simply checks infeed 'Status' and 'State'. Can also query other metrics.\n";
   print "\t   The following status metrics are currently available:\n";
   print "\t   State,Status,CurrentStatus\n";
   print "\t   The following metered metrics are currently available:\n";
   print "\t   CurrentCapacity,Current,CurrentUtilized\n\n";

   print "\t--contact\n";
   print "\t   With no arguments, simply checks status of all available contact closure sensors\n";
   print "\t   A comma separated list of contact closure sensors may be specified to monitor.\n";
   print "\t   NOTE: The IDs are 'simplified', so only use 1-4. Not E1,C1, etc. \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) 2016 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;

}

# 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 the standard SNMP sysObjectID which is used to detect Sentry3 vs. Sentry4 products
   my $sysObjectID = $session->get_request( varbindlist => ['.1.3.6.1.2.1.1.2.0'],);
   if (!defined $sysObjectID) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   if ($sysObjectID->{'.1.3.6.1.2.1.1.2.0'} eq '.1.3.6.1.4.1.1718.3') {
      $sentryVersion = 3;
   }
   elsif ($sysObjectID->{'.1.3.6.1.2.1.1.2.0'} eq '.1.3.6.1.4.1.1718.4') {
      $sentryVersion = 4;
   }
   else {
      &exit_unknown("UNKNOWN - Unrecognized Sentry CDU Version");
   }

   if ($sentryVersion eq 3) {
      # Fetch generic system information from Sentry3 prodcuts via SNMP
      my $systemTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.1718.3.1');
      if (!defined $systemTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

      $systemVersion = $systemTable->{'.1.3.6.1.4.1.1718.3.1.1.0'};
      $systemLocation = $systemTable->{'.1.3.6.1.4.1.1718.3.1.3.0'};
      $systemTotalPower = $systemTable->{'.1.3.6.1.4.1.1718.3.1.6.0'};
   }

   if ($sentryVersion eq 4) {
      # Fetch generic system information from Sentry4 products via SNMP
      my $st4SystemTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.1718.4.1.1.1');
      if (!defined $st4SystemTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

      $st4SystemProductName = $st4SystemTable->{'.1.3.6.1.4.1.1718.4.1.1.1.1.0'};
      $st4SystemFirmwareVersion = $st4SystemTable->{'.1.3.6.1.4.1.1718.4.1.1.1.3.0'};
      $systemVersion = $st4SystemProductName . " " . $st4SystemFirmwareVersion;

      $systemLocation = $st4SystemTable->{'.1.3.6.1.4.1.1718.4.1.1.1.2.0'};
   }

}

# 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;

}

# Function used to set thresholds. Called from sanitize functions.
sub set_thresholds() {

   my($device_queries, $type) = @_;
 
   # If there are queries but no warning or critical thresholds, that is a problem. We don't do any
   # automatic thresholds for cord stats.
   if ( !(@$device_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 @{$device_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 @{$device_queries}) > 1) {
         &exit_unknown("UNKNOWN - single threshold is ambiguous when querying multiple $type 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 @{$device_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 @{$device_queries}) > 1) {
         &exit_unknown("UNKNOWN - single threshold is ambiguous when querying multiple $type 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 @{$device_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]));
      }
   }
}




#
# Temperature conversion functions
#

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

# 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);
}




#
# SENTRY4 DATA ACQUISITION
#

# Sentry4 build cord data
sub build_cord_data() {

   # Get a short list of the available cords. This just makes my life easier.
   my $cord = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.3.2.1.2');
   if (!defined $cord) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Now get the entire tower table. This includes what I already got. I don't care.
   my $cordTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.3');
   if (!defined $cordTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

   foreach my $cord (@cords) {
      while (my ($key, $value) = each %{$cordTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.2.1.2' . $cord) ) { $cord_data{$cord}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.2.1.3' . $cord) ) { $cord_data{$cord}->{'Name'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.2.1.12' . $cord) ) { $cord_data{$cord}->{'PowerCapacity'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.1' . $cord) ) { $cord_data{$cord}->{'State'} = &translate(\@st4_global_device_state, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.2' . $cord) ) { $cord_data{$cord}->{'Status'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.3' . $cord) ) { $cord_data{$cord}->{'ActivePower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.4' . $cord) ) { $cord_data{$cord}->{'ActivePowerStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.5' . $cord) ) { $cord_data{$cord}->{'ApparentPower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.6' . $cord) ) { $cord_data{$cord}->{'ApparentPowerStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.7' . $cord) ) { $cord_data{$cord}->{'PowerUtilized'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.8' . $cord) ) { $cord_data{$cord}->{'PowerFactor'} = ($value /100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.9' . $cord) ) { $cord_data{$cord}->{'PowerFactorStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.10' . $cord) ) { $cord_data{$cord}->{'Energy'} = ($value /10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.11' . $cord) ) { $cord_data{$cord}->{'Frequency'} = ($value /10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.12' . $cord) ) { $cord_data{$cord}->{'OutOfBalance'} = ($value /10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.3.1.13' . $cord) ) { $cord_data{$cord}->{'OutOfBalanceStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.2' . $cord) ) { $cord_data{$cord}->{'ActivePowerLowAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.3' . $cord) ) { $cord_data{$cord}->{'ActivePowerLowWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.4' . $cord) ) { $cord_data{$cord}->{'ActivePowerHighWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.5' . $cord) ) { $cord_data{$cord}->{'ActivePowerHighAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.6' . $cord) ) { $cord_data{$cord}->{'ApparentPowerLowAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.7' . $cord) ) { $cord_data{$cord}->{'ApparentPowerLowWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.8' . $cord) ) { $cord_data{$cord}->{'ApparentPowerHighWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.9' . $cord) ) { $cord_data{$cord}->{'ApparentPowerHighAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.10' . $cord) ) { $cord_data{$cord}->{'PowerFactorLowAlarm'} = ($value /100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.11' . $cord) ) { $cord_data{$cord}->{'PowerFactorLowWarning'} = ($value /100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.12' . $cord) ) { $cord_data{$cord}->{'OutOfBalanceHighWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.3.4.1.13' . $cord) ) { $cord_data{$cord}->{'OutOfBalanceHighAlarm'} = $value }

      }
   }
}

# Sentry4 build line data
sub build_line_data() {

   # Get a short list of the available lines. This just makes my life easier.
   my $line = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.4.2.1.2');
   if (!defined $line) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Now get the entire line table. This includes what I already got. I don't care.
   my $lineTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.4');
   if (!defined $lineTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

   foreach my $line (@lines) {
      while (my ($key, $value) = each %{$lineTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.2.1.2' . $line) ) { $line_data{$line}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.2.1.4' . $line) ) { $line_data{$line}->{'Label'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.2.1.6' . $line) ) { $line_data{$line}->{'CurrentCapacity'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.3.1.1' . $line) ) { $line_data{$line}->{'State'} = &translate(\@st4_global_device_state, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.3.1.2' . $line) ) { $line_data{$line}->{'Status'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.3.1.3' . $line) ) { $line_data{$line}->{'Current'} = ($value/100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.3.1.4' . $line) ) { $line_data{$line}->{'CurrentStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.3.1.5' . $line) ) { $line_data{$line}->{'CurrentUtilized'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.4.1.2' . $line) ) { $line_data{$line}->{'CurrentLowAlarm'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.4.1.3' . $line) ) { $line_data{$line}->{'CurrentLowWarning'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.4.1.4' . $line) ) { $line_data{$line}->{'CurrentHighWarning'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.4.4.1.5' . $line) ) { $line_data{$line}->{'CurrentHighAlarm'} = ($value/10) }
      }
   }
}

# Sentry4 build phase data
sub build_phase_data() {

   # Get a short list of the available phases. This just makes my life easier.
   my $phase = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.5.2.1.2');
   if (!defined $phase) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Now get the entire line table. This includes what I already got. I don't care.
   my $phaseTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.5');
   if (!defined $phaseTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

   foreach my $phase (@phases) {
      while (my ($key, $value) = each %{$phaseTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.2.1.2' . $phase) ) { $phase_data{$phase}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.2.1.4' . $phase) ) { $phase_data{$phase}->{'Label'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.1' . $phase) ) { $phase_data{$phase}->{'State'} = &translate(\@st4_global_device_state, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.2' . $phase) ) { $phase_data{$phase}->{'Status'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.3' . $phase) ) { $phase_data{$phase}->{'Voltage'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.4' . $phase) ) { $phase_data{$phase}->{'VoltageStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.5' . $phase) ) { $phase_data{$phase}->{'VoltageDeviation'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.6' . $phase) ) { $phase_data{$phase}->{'Current'} = ($value/100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.7' . $phase) ) { $phase_data{$phase}->{'CrestFactor'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.8' . $phase) ) { $phase_data{$phase}->{'ActivePower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.9' . $phase) ) { $phase_data{$phase}->{'ApparentPower'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.10' . $phase) ) { $phase_data{$phase}->{'PowerFactor'} = ($value/100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.11' . $phase) ) { $phase_data{$phase}->{'PowerFactorStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.12' . $phase) ) {
            my @val_array = qw(unknown capacitive inductive resistive);
            $phase_data{$phase}->{'Reactance'} = &translate(\@val_array, $value);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.3.1.13' . $phase) ) { $phase_data{$phase}->{'Energy'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.4.1.2' . $phase) ) { $phase_data{$phase}->{'VoltageLowAlarm'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.4.1.3' . $phase) ) { $phase_data{$phase}->{'VoltageLowWarning'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.4.1.4' . $phase) ) { $phase_data{$phase}->{'VoltageHighWarning'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.4.1.5' . $phase) ) { $phase_data{$phase}->{'VoltageHighAlarm'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.4.1.6' . $phase) ) { $phase_data{$phase}->{'PowerFactorLowAlarm'} = ($value/100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.5.4.1.7' . $phase) ) { $phase_data{$phase}->{'PowerFactorLowWarning'} = ($value/100) }
      }
   }
}

# Sentry4 build branch data
sub build_branch_data() {

   # Get a short list of the available phases. This just makes my life easier.
   my $branch = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.7.2.1.2');
   if (!defined $branch) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Now get the entire branch table. This includes what I already got. I don't care.
   my $branchTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.7');
   if (!defined $branchTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

   foreach my $branch (@branches) {
      while (my ($key, $value) = each %{$branchTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.2.1.2' . $branch) ) { $branch_data{$branch}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.2.1.4' . $branch) ) { $branch_data{$branch}->{'Label'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.2.1.6' . $branch) ) { $branch_data{$branch}->{'CurrentCapacity'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.2.1.20' . $branch) ) { $branch_data{$branch}->{'PhaseID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.3.1.1' . $branch) ) { $branch_data{$branch}->{'State'} = &translate(\@st4_global_device_state, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.3.1.2' . $branch) ) { $branch_data{$branch}->{'Status'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.3.1.3' . $branch) ) { $branch_data{$branch}->{'Current'} = ($value/100) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.3.1.4' . $branch) ) { $branch_data{$branch}->{'CurrentStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.3.1.5' . $branch) ) { $branch_data{$branch}->{'CurrentUtilized'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.4.1.2' . $branch) ) { $branch_data{$branch}->{'CurrentLowAlarm'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.4.1.3' . $branch) ) { $branch_data{$branch}->{'CurrentLowWarning'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.4.1.4' . $branch) ) { $branch_data{$branch}->{'CurrentHighWarning'} = ($value/10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.7.4.1.5' . $branch) ) { $branch_data{$branch}->{'CurrentHighAlarm'} = ($value/10) }
      }
   }
}

# Sentry4 Contact Sensor Data Acquisition
sub build_sentry4_contact_data() {
   
   # Get a short list of the available cords. This just makes my life easier.
   my $contact = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.12.2.1.2');
   if (!defined $contact) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }
   
   # Now get the entire tower table. This includes what I already got. I don't care.
   my $contactTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.12');
   if (!defined $contactTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

   foreach my $contact (@contacts) {
      while (my ($key, $value) = each %{$contactTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.12.2.1.2' . $contact) ) {
            $contact_data{$contact}->{'ID'} = $value;
            $contact_data{$contact}->{'simpleID'} = substr($contact_data{$contact}->{'ID'}, 1);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.12.2.1.3' . $contact) ) { $contact_data{$contact}->{'Name'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.12.3.1.1' . $contact) ) {
            $contact_data{$contact}->{'State'} = &translate(\@st4_global_device_status, $value);
         }

      }
   }
}

# Sentry4 build temp/humid data
# 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 st4_build_sensor_data {


   # Fetch the system-wide Temperature Sensor Scale
   my $st4TempSensorScaleObject = $session->get_request( varbindlist => ['.1.3.6.1.4.1.1718.4.1.9.1.10.0'],);
   if (!defined $st4TempSensorScaleObject) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   if ($st4TempSensorScaleObject->{'.1.3.6.1.4.1.1718.4.1.9.1.10.0'} eq '0') {
      $st4TempSensorScale = "celsius";
   }
   elsif ($st4TempSensorScaleObject->{'.1.3.6.1.4.1.1718.4.1.9.1.10.0'} eq '1') {
      $st4TempSensorScale = "fahrenheit";
   }
   else {
      &exit_unknown("UNKNOWN - Unrecognized Sentry4 Temperature Sensor Scale");
   }

   # Get just the list of sensors on the CDU.
   my $st4TempSensorID = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.4.1.9.2.1.2');
   if (!defined $st4TempSensorID) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Get the entire temperature table.
   my $st4TempTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.1718.4.1.9');
   if (!defined $st4TempTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Get the entire humidity table.
   my $st4HumidTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.1718.4.1.10');
   if (!defined $st4HumidTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

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

   foreach my $sensor (@sensors) {

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

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.2.1.2' . $sensor) ) { $sensor_data{$sensor}->{'ID'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.2.1.3' . $sensor) ) { $sensor_data{$sensor}->{'TempName'} = $value }
         # NOTE: Sentry4 units combine both "statuses" together ... so physical sensor problems (lost, found) and
         # actual environment issues (highWarning, highAlarm) are all reported through this single metric. 
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.3.1.2' . $sensor) ) { $sensor_data{$sensor}->{'TempStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.3.1.1' . $sensor) ) { $sensor_data{$sensor}->{'TempValue'} = ($value / 10) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.4.1.2' . $sensor) ) { $sensor_data{$sensor}->{'TempLowAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.4.1.3' . $sensor) ) { $sensor_data{$sensor}->{'TempLowWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.4.1.4' . $sensor) ) { $sensor_data{$sensor}->{'TempHighWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.9.4.1.5' . $sensor) ) { $sensor_data{$sensor}->{'TempHighAlarm'} = $value }

         # Set the Temp Sensor Scale 
         $sensor_data{$sensor}->{'TempScale'} = $st4TempSensorScale; 

      }
   }

   foreach my $sensor (@sensors) {

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

         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.2.1.3' . $sensor) ) { $sensor_data{$sensor}->{'HumidName'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.3.1.2' . $sensor) ) { $sensor_data{$sensor}->{'HumidStatus'} = &translate(\@st4_global_device_status, $value) }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.3.1.1' . $sensor) ) { $sensor_data{$sensor}->{'HumidValue'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.4.1.2' . $sensor) ) { $sensor_data{$sensor}->{'HumidLowAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.4.1.3' . $sensor) ) { $sensor_data{$sensor}->{'HumidLowWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.4.1.4' . $sensor) ) { $sensor_data{$sensor}->{'HumidHighWarning'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.4.1.10.4.1.5' . $sensor) ) { $sensor_data{$sensor}->{'HumidHighAlarm'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.5.1.14' . $sensor) ) { $sensor_data{$sensor}->{'TempRecDelta'} = $value }
      }
   }
}
 



#
# SENTRY3 DATA ACQUISITION
#

# Sentry3 build infeed data
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) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->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) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->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(.*)/;
      push(@infeeds, $1);
   }

   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) ) { $infeed_data{$infeed}->{'LoadValue'} = ($value / 100) }
         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) ) { $infeed_data{$infeed}->{'Voltage'} = ($value / 10) }
         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) ) { $infeed_data{$infeed}->{'PowerFactor'} = ($value / 100) }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.15' . $infeed) ) { $infeed_data{$infeed}->{'CrestFactor'} = ($value / 10) }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.16' . $infeed) ) { $infeed_data{$infeed}->{'Energy'} = ($value / 10) }
         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) ) { $infeed_data{$infeed}->{'PhaseVoltage'} = ($value / 10) }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.19' . $infeed) ) { $infeed_data{$infeed}->{'PhaseCurrent'} = ($value / 100) }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.2.1.20' . $infeed) ) { $infeed_data{$infeed}->{'CapacityUsed'} = ($value /10) }
         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 }

      }
   }
}

# Sentry3 build tower data
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) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->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 $towerTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->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(.*)/;
      push(@towers, $1);
   }

   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) ) { $tower_data{$tower}->{'VACapacityUsed'} = ($value /10) }
         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) ) { $tower_data{$tower}->{'PowerFactor'} = ($value / 100) }
         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 }
      }
   }
}

# Sentry3 Contact Sensor Data Acquisition
sub build_sentry3_contact_data() {

   # Get a short list of the available contacts. This just makes my life easier.
   my $contact = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.6.1.2');
   if (!defined $contact) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

   # Now get the entire tower table. This includes what I already got. I don't care.
   my $contactTable = $session->get_table( baseoid => '.1.3.6.1.4.1.1718.3.2.6.1');
   if (!defined $contactTable) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->error()) }

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

   foreach my $contact (@contacts) {
      while (my ($key, $value) = each %{$contactTable}) {

         if ($key eq ('.1.3.6.1.4.1.1718.3.2.6.1.2' . $contact) ) {
            $contact_data{$contact}->{'ID'} = $value;
            $contact_data{$contact}->{'simpleID'} = substr($contact_data{$contact}->{'ID'}, 1);
         }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.6.1.3' . $contact) ) { $contact_data{$contact}->{'Name'} = $value }
         if ($key eq ('.1.3.6.1.4.1.1718.3.2.6.1.4' . $contact) ) {
            my @val_array = qw(normal alarm noComm);
            $contact_data{$contact}->{'State'} = &translate(\@val_array, $value);
         }
      }
   }
}

# Sentry 3 build temp/humid data
# 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) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->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) { &exit_unknown("UNKNOWN - SNMP ERROR: " . $session->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(.*)/;
      push(@sensors, $1);
   }   

   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) ) { $sensor_data{$sensor}->{'TempValue'} = ($value / 10) }
         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 }
      }
   }
}




# 
# SENTRY4 DATA PROCESSING
#

# Sentry4 process cord data
sub process_cord_data() {

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

      # This is a bare request with no arguments. Do the standard checks.
      if ($cord_queries[0] eq '') {
         # If the cord is beat up, it's sensical to report that now, and skip tossing unitialized erros below.
         # In the past I knew which statuses were possible for a given device. Now, with the Sentry4 global device status
         # there is no way to really know ... so anything other than "normal(0)" will be WARNING for now. This is subject
         # to change in the future.
         if (($cord_data{$cordid}->{'Status'} =~ /\bnormal\b/) && ($cord_data{$cordid}->{'State'} =~ /\bon\b/)) { push(@OK,$msg_txt) }
         else { push(@WARNING, $msg_txt) }
      } 

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

            # If the cord has noComm, bug out with an UNKNOWN and skip any checks for the cord.
            # They will all be false anyway. We use UNKNOWN here, because it is assumed that
            # another check is already being done on just the cord 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 ($cord_data{$cordid}->{'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($cord_data{$cordid}->{$cord_queries[$i]})) {
                  &exit_unknown("UNKNOWN - It appears that cord metric '$cord_queries[$i]' is not available on this unit");
               }

               # Handle checks for 'status' objects differently..
               if ($cord_queries[$i] =~ /ApparentPowerStatus|ActivePowerStatus|PowerFactorStatus|OutOfBalanceStatus/) { 
                   $msg_txt = $cord_data{$cordid}->{'Name'} . "(" . $cord_data{$cordid}->{'ID'} . ") " . $cord_queries[$i] .": " . $cord_data{$cordid}->{$cord_queries[$i]};
                   if ($cord_data{$cordid}->{$cord_queries[$i]} =~ /\bnormal\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               # Any checks which use a threshold.
               else {
                  # 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 ($cord_queries[$i] =~ /PowerCapacity/) { $unit = "VA" }
                  elsif ($cord_queries[$i] =~ /ActivePower/) { $unit ="W" }
                  elsif ($cord_queries[$i] =~ /ApparentPower/) { $unit ="VA" }
                  elsif ($cord_queries[$i] =~ /PowerUtilized/) {  $unit = "%" }
                  elsif ($cord_queries[$i] =~ /Energy/) { $unit = "kWh" }
                  elsif ($cord_queries[$i] =~ /Frequency/) { $unit = "Hz" }
                  elsif ($cord_queries[$i] =~ /OutOfBalance/) { $unit = "%" }
                  else { $unit = "" }

                  # Extra fail-safe. Sometimes we end up here, unfortunately. 
                  if (!(defined($threshold_array[$i]))) {
                     &exit_unknown("UNKNOWN - A threshold is missing.");
                  }

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

# Sentry4 process line data
sub process_line_data() {

   my $unit;
   foreach my $lineid (@lines) {
      # Standard message text for most alerts.
      my $msg_txt = $line_data{$lineid}->{'Label'} . "(" . $line_data{$lineid}->{'ID'} . ") " . "Status: " . $line_data{$lineid}->{'Status'} . " State: " . $line_data{$lineid}->{'State'};

      # This is a bare request with no arguments. Do the standard checks.
      if ($line_queries[0] eq '') {
         # If the cord is beat up, it's sensical to report that now, and skip tossing unitialized erros below.
         # In the past I knew which statuses were possible for a given device. Now, with the Sentry4 global device status
         # there is no way to really know ... so anything other than "normal(0)" will be WARNING for now. This is subject
         # to change in the future.
         if (($line_data{$lineid}->{'Status'} =~ /\bnormal\b/) && ($line_data{$lineid}->{'State'} =~ /\bon\b/)) { push(@OK,$msg_txt) }
         else { push(@WARNING, $msg_txt) }
      } 

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

            # If the line has noComm, bug out with an UNKNOWN and skip any checks for the line.
            # They will all be false anyway. We use UNKNOWN here, because it is assumed that
            # another check is already being done on just the line 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 ($line_data{$lineid}->{'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($line_data{$lineid}->{$line_queries[$i]})) {
                  &exit_unknown("UNKNOWN - It appears that line metric '$line_queries[$i]' is not available on this unit");
               }

               # Handle checks for 'status' objects differently..
               if ($line_queries[$i] =~ /Status|CurrentStatus/) { 
                   $msg_txt = $line_data{$lineid}->{'Label'} . "(" . $line_data{$lineid}->{'ID'} . ") " . $line_queries[$i] .": " . $line_data{$lineid}->{$line_queries[$i]};
                   if ($line_data{$lineid}->{$line_queries[$i]} =~ /\bnormal\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               elsif ($line_queries[$i] =~ /State/) { 
                   $msg_txt = $line_data{$lineid}->{'Label'} . "(" . $line_data{$lineid}->{'ID'} . ") " . $line_queries[$i] .": " . $line_data{$lineid}->{$line_queries[$i]};
                   if ($line_data{$lineid}->{$line_queries[$i]} =~ /\bon\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               # Any checks which use a threshold.
               else {
                  # 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 ($line_queries[$i] =~ /Current\b/) { $unit = "A" }
                  elsif ($line_queries[$i] =~ /CurrentUtilized/) { $unit = "%" }
                  else { $unit = "" }

                  # Extra fail-safe. Sometimes we end up here, unfortunately. 
                  if (!(defined($threshold_array[$i]))) {
                     &exit_unknown("UNKNOWN - A threshold is missing.");
                  }

                  my $line_status = $threshold_array[$i]->get_status($line_data{$lineid}->{$line_queries[$i]});
                  my $msg_txt = $line_data{$lineid}->{'Label'} . "(" . $line_data{$lineid}->{'ID'} . ") " . $line_queries[$i] .": " . $line_data{$lineid}->{$line_queries[$i]} . $unit;
                  if ($line_status eq 0) { push(@OK,$msg_txt) }
                  if ($line_status eq 1) { push(@WARNING,$msg_txt) }
                  if ($line_status eq 2) { push(@CRITICAL,$msg_txt) }
               }
            }
         }
      }
   }      
}

# Sentry4 process phase data
sub process_phase_data() {

   my $unit;
   foreach my $phaseid (@phases) {
      # Standard message text for most alerts.
      my $msg_txt = $phase_data{$phaseid}->{'Label'} . "(" . $phase_data{$phaseid}->{'ID'} . ") " . "Status: " . $phase_data{$phaseid}->{'Status'} . " State: " . $phase_data{$phaseid}->{'State'};

      # This is a bare request with no arguments. Do the standard checks.
      if ($phase_queries[0] eq '') {
         # If the cord is beat up, it's sensical to report that now, and skip tossing unitialized erros below.
         # In the past I knew which statuses were possible for a given device. Now, with the Sentry4 global device status
         # there is no way to really know ... so anything other than "normal(0)" will be WARNING for now. This is subject
         # to change in the future.
         if (($phase_data{$phaseid}->{'Status'} =~ /\bnormal\b/) && ($phase_data{$phaseid}->{'State'} =~ /\bon\b/)) { push(@OK,$msg_txt) }
         else { push(@WARNING, $msg_txt) }
      } 

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

            # If the cord has noComm, bug out with an UNKNOWN and skip any checks for the cord.
            # They will all be false anyway. We use UNKNOWN here, because it is assumed that
            # another check is already being done on just the cord 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 ($phase_data{$phaseid}->{'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($phase_data{$phaseid}->{$phase_queries[$i]})) {
                  &exit_unknown("UNKNOWN - It appears that phase metric '$phase_queries[$i]' is not available on this unit");
               }

               # Handle checks for 'status' objects differently..
               if ($phase_queries[$i] =~ /Status|VoltageStatus|PowerFactorStatus/) { 
                   $msg_txt = $phase_data{$phaseid}->{'Label'} . "(" . $phase_data{$phaseid}->{'ID'} . ") " . $phase_queries[$i] .": " . $phase_data{$phaseid}->{$phase_queries[$i]};
                   if ($phase_data{$phaseid}->{$phase_queries[$i]} =~ /\bnormal\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               elsif ($phase_queries[$i] =~ /State/) { 
                   $msg_txt = $phase_data{$phaseid}->{'Label'} . "(" . $phase_data{$phaseid}->{'ID'} . ") " . $phase_queries[$i] .": " . $phase_data{$phaseid}->{$phase_queries[$i]};
                   if ($phase_data{$phaseid}->{$phase_queries[$i]} =~ /\bon\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               elsif ($phase_queries[$i] =~ /Reactance/) { 
                   $msg_txt = $phase_data{$phaseid}->{'Label'} . "(" . $phase_data{$phaseid}->{'ID'} . ") " . $phase_queries[$i] .": " . $phase_data{$phaseid}->{$phase_queries[$i]};
                   # 20170718: Added "unknown" here since typically this indicates there is no reactance, since nothing is plugged in. 
                   if ($phase_data{$phaseid}->{$phase_queries[$i]} =~ /\bcapacitive\b||\bunknown\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               # Any checks which use a threshold.
               else {
                  # 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 ($phase_queries[$i] =~ /Voltage\b/) { $unit = "V" }
                  elsif ($phase_queries[$i] =~ /VoltageDeviation/) {  $unit = "%" }
                  elsif ($phase_queries[$i] =~ /Current/) { $unit ="A" }
                  elsif ($phase_queries[$i] =~ /CrestFactor/) { $unit = "" }
                  elsif ($phase_queries[$i] =~ /ActivePower/) { $unit = "W" }
                  elsif ($phase_queries[$i] =~ /ApparentPower/) { $unit = "VA" }
                  elsif ($phase_queries[$i] =~ /PowerFactor/) { $unit = "" }
                  elsif ($phase_queries[$i] =~ /Energy/) { $unit = "kWh" }
                  else { $unit = "" }

                  # Extra fail-safe. Sometimes we end up here, unfortunately. 
                  if (!(defined($threshold_array[$i]))) {
                     &exit_unknown("UNKNOWN - A threshold is missing.");
                  }

                  my $phase_status = $threshold_array[$i]->get_status($phase_data{$phaseid}->{$phase_queries[$i]});
                  my $msg_txt = $phase_data{$phaseid}->{'Label'} . "(" . $phase_data{$phaseid}->{'ID'} . ") " . $phase_queries[$i] .": " . $phase_data{$phaseid}->{$phase_queries[$i]} . $unit;
                  
                  # 20180312: Cheap trick to ignore PowerFactor readings less than 0, aka "-0.01" for an unloaded PDU.
                  # It's not easy (or possible really) to set a sensible PowerFactor threshold while accounting for a negative value as OK. 
                  if (($phase_queries[$i] =~ /PowerFactor/) && ($phase_data{$phaseid}->{$phase_queries[$i]} < 0)) {
                     $phase_status = 0;
                  }

                  if ($phase_status eq 0) { push(@OK,$msg_txt) }
                  if ($phase_status eq 1) { push(@WARNING,$msg_txt) }
                  if ($phase_status eq 2) { push(@CRITICAL,$msg_txt) }
               }
            }
         }
      }
   }      
}

# Sentry4 process branch data
sub process_branch_data() {

   my $unit;
   foreach my $branchid (@branches) {
      # Standard message text for most alerts.
      my $msg_txt = $branch_data{$branchid}->{'Label'} . "(" . $branch_data{$branchid}->{'ID'} . ") " . "Status: " . $branch_data{$branchid}->{'Status'} . " State: " . $branch_data{$branchid}->{'State'};

      # This is a bare request with no arguments. Do the standard checks.
      if ($branch_queries[0] eq '') {
         # If the cord is beat up, it's sensical to report that now, and skip tossing unitialized erros below.
         # In the past I knew which statuses were possible for a given device. Now, with the Sentry4 global device status
         # there is no way to really know ... so anything other than "normal(0)" will be WARNING for now. This is subject
         # to change in the future.
         if (($branch_data{$branchid}->{'Status'} =~ /\bnormal\b/) && ($branch_data{$branchid}->{'State'} =~ /\bon\b/)) { push(@OK,$msg_txt) }
         else { push(@WARNING, $msg_txt) }
      } 

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

            # If the branch has noComm, bug out with an UNKNOWN and skip any checks for the cord.
            # They will all be false anyway. We use UNKNOWN here, because it is assumed that
            # another check is already being done on just the cord 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 ($branch_data{$branchid}->{'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($branch_data{$branchid}->{$branch_queries[$i]})) {
                  &exit_unknown("UNKNOWN - It appears that branch metric '$branch_queries[$i]' is not available on this unit");
               }

               # Handle checks for 'status' objects differently..
               if ($branch_queries[$i] =~ /Status|CurrentStatus/) { 
                   $msg_txt = $branch_data{$branchid}->{'Label'} . "(" . $branch_data{$branchid}->{'ID'} . ") " . $branch_queries[$i] .": " . $branch_data{$branchid}->{$branch_queries[$i]};
                   if ($branch_data{$branchid}->{$branch_queries[$i]} =~ /\bnormal\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               elsif ($branch_queries[$i] =~ /State/) { 
                   $msg_txt = $branch_data{$branchid}->{'Label'} . "(" . $branch_data{$branchid}->{'ID'} . ") " . $branch_queries[$i] .": " . $branch_data{$branchid}->{$branch_queries[$i]};
                   if ($branch_data{$branchid}->{$branch_queries[$i]} =~ /\bon\b/) { push(@OK,$msg_txt) }
                   else { push(@WARNING,$msg_txt) }
               }

               # Any checks which use a threshold.
               else {
                  # 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 ($branch_queries[$i] =~ /Current\b/) { $unit = "A" }
                  elsif ($branch_queries[$i] =~ /CurrentUtilized/) {  $unit = "%" }
                  else { $unit = "" }

                  # Extra fail-safe. Sometimes we end up here, unfortunately. 
                  if (!(defined($threshold_array[$i]))) {
                     &exit_unknown("UNKNOWN - A threshold is missing.");
                  }

                  my $branch_status = $threshold_array[$i]->get_status($branch_data{$branchid}->{$branch_queries[$i]});
                  my $msg_txt = $branch_data{$branchid}->{'Label'} . "(" . $branch_data{$branchid}->{'ID'} . ") " . $branch_queries[$i] .": " . $branch_data{$branchid}->{$branch_queries[$i]} . $unit;
                  if ($branch_status eq 0) { push(@OK,$msg_txt) }
                  if ($branch_status eq 1) { push(@WARNING,$msg_txt) }
                  if ($branch_status eq 2) { push(@CRITICAL,$msg_txt) }
               }
            }
         }
      }
   }      
}

# Sentry4 temp/humid data processing
# Process the sensor data collected over SNMP, apply various options and thresholds.
# Determine which sensors should be queried, and record status information.
sub st4_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 ($TempLowAlarm, $TempLowWarning, $TempHighWarning, $TempHighAlarm, $TempValue, $temp_warning_string, $temp_critical_string, $humid_warning_string, $humid_critical_string, $TempUnit, $DewValue, $DewUnit, $dewpoint);

      # Base text to use in all temperature messages
      my $msg_base_temp = $sensor_data{$sensor}->{'TempName'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . ": ";
      # Base text to use in all humidity messages
      my $msg_base_humid = $sensor_data{$sensor}->{'HumidName'} . "(" . $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/) ) {
         $TempLowAlarm = t_convert_c_to_f($sensor_data{$sensor}->{'TempLowAlarm'});
         $TempLowWarning = t_convert_c_to_f($sensor_data{$sensor}->{'TempLowWarning'});
         $TempHighWarning = t_convert_c_to_f($sensor_data{$sensor}->{'TempHighWarning'});
         $TempHighAlarm = t_convert_c_to_f($sensor_data{$sensor}->{'TempHighAlarm'});
         $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/) ) {
         $TempLowAlarm = t_convert_f_to_c($sensor_data{$sensor}->{'TempLowAlarm'});
         $TempLowWarning = t_convert_f_to_c($sensor_data{$sensor}->{'TempLowWarning'});
         $TempHighWarning = t_convert_f_to_c($sensor_data{$sensor}->{'TempHighWarning'});
         $TempHighAlarm = t_convert_f_to_c($sensor_data{$sensor}->{'TempHighAlarm'});
         $TempValue = t_convert_f_to_c($sensor_data{$sensor}->{'TempValue'});
         $TempUnit ="C";
      }

     # The requested temperature scale matches the configured temperature scale.
      else {
         $TempLowAlarm = $sensor_data{$sensor}->{'TempLowAlarm'};
         $TempLowWarning = $sensor_data{$sensor}->{'TempLowWarning'};
         $TempHighWarning = $sensor_data{$sensor}->{'TempHighWarning'};
         $TempHighAlarm = $sensor_data{$sensor}->{'TempHighAlarm'};
         $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. New Sentry4 products implement a 'Warning' and 
      # 'Alarm' threshold, so these are translated to nagios Warning / Critical below. 
      if ( !defined($user_temp_threshold_set)  ) {
         $temp_warning_string = $TempLowWarning . ":" . $TempHighWarning;
         $temp_critical_string = $TempLowAlarm . ":" . $TempHighAlarm;
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds( warning => $temp_warning_string, critical => $temp_critical_string);
      }

      if ( !defined($user_humid_threshold_set) ) {
         $humid_warning_string = $sensor_data{$sensor}->{'HumidLowWarning'} . ":" . $sensor_data{$sensor}->{'HumidHighWarning'};
         $humid_critical_string = $sensor_data{$sensor}->{'HumidLowAlarm'} . ":" . $sensor_data{$sensor}->{'HumidHighAlarm'};
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds( warning => $humid_warning_string, critical => $humid_critical_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.
         # Sentry4 Update: I have no idea which gobal DeviceStatus states are used for sensors anymore. I am 
         # hoping that these same states are OK to use here. Easy to change if not ...
         if ($sensor_data{$sensor}->{'TempStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base_temp . $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_temp . $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.
         # Sentry4 Update: Remember I don't know which states can apply to these sensors now.
         if ($sensor_data{$sensor}->{'HumidStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base_humid . $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_humid . $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.
         # Sentry4 Update: See above.
         if ($sensor_data{$sensor}->{'HumidStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base_temp . $sensor_data{$sensor}->{'HumidStatus'};
            push(@UNKNOWN, $msg_txt);
         }

         elsif ($sensor_data{$sensor}->{'TempStatus'} =~ /notFound|readError|lost|noComm/) {
            my $msg_txt = $msg_base_temp . $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_temp . " 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}->{'TempName'} . "(" . $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}->{'TempName'} . "(" . $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) }
            }
         }
      }
   }
}




#
# SENTRY3 & SENTRY4 DATA PROCESSING
#

# Process contact closure data
sub process_contact_data() {
   my @contacts_to_query;
   my $found = 0;
   if (@contacts_list) {
      foreach my $contactq (@contacts_list) {
         $found = 0;
         foreach my $contact (@contacts) {
            if ($contactq eq $contact_data{$contact}->{'simpleID'}) {
               push(@contacts_to_query,$contact);
               $found = 1;
            }
         }
         if(!$found) {
            &exit_unknown("UNKNOWN - Contact ID $contactq not present!");
         }
      }
   }
  
   else {
      @contacts_to_query = @contacts;
   }

   foreach my $contact (@contacts_to_query) {
      my $msg_txt = $contact_data{$contact}->{'Name'} . "(" . $contact_data{$contact}->{'ID'} . ")" .": " . $contact_data{$contact}->{'State'};
      if ($contact_data{$contact}->{'State'} =~ /normal/) { push(@OK,$msg_txt) }
      else { push(@WARNING,$msg_txt) }
   }
}




#
# SENTRY3 DATA PROCESSING
#

# Sentry3 process infeed data
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.
                           # 20170718: I did this a bit further below, and it was a real hack.
                           $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);
                     # 20170718: This is a hack to essentially disable load imbalance checks when the average load on an infeed
                     # is so small it's impossible to manage and pointless to alert on. Sadly it will report 0.00% in the plugin 
                     # output but I just simply don't have time to do a better job at this. 
                     if ($average < $IGNORE_LOAD_IMBAL_THRESHOLD) { $deviation = 0 }
                     elsif ($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) }
               }
            }
         }
      }
   }
}

# Sentry3 process tower data
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) }
            }
         }
      }
   }      
}

# Sentry3 process temp/humid data
# 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) }
            }
         }
      }
   }
}




#
# SENTRY4 SET THRESHOLDS
#

# Sentry4 cord user thresholds
sub sanitize_cord_set_thresholds() {

   # There are certain queries that cannot be mixed with ones that require thresholds. I'm simply 
   # not going to expend the effort to support this.
   foreach my $query (@cord_queries) {
      if ($query =~ /ApparentPowerStatus|ActivePowerStatus|PowerFactorStatus|OutOfBalanceStatus/) {
         &exit_unknown("UNKNOWN - Cord metric '$query' cannot be included when passing thresholds for other metrics on the command line. Sorry. Setup a separate check for it.");
      }
   }
  
   # 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 (@cord_queries) {
      if (! ($query =~ /PowerCapacity|ActivePower|ApparentPower|PowerUtilized|PowerFactor|Energy|Frequency|OutOfBalance/) && !($query eq'')) {
         &exit_unknown("UNKNOWN - Cord metric '$query' is not currently supported by this plugin.");
      }
   }

   &set_thresholds(\@cord_queries,"cord");
}

# Sentry4 line user thresholds
sub sanitize_line_set_thresholds() {

   # There are certain queries that cannot be mixed with ones that require thresholds. I'm simply 
   # not going to expend the effort to support this.
   foreach my $query (@line_queries) {
      if ($query =~ /Status|State|CurrentStatus/) {
         &exit_unknown("UNKNOWN - Line metric '$query' cannot be included when passing thresholds for other metrics on the command line. Sorry. Setup a separate check for it.");
      }
   }
  
   # 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 (@line_queries) {
      if (! ($query =~ /CurrentUtilized|Current/) && !($query eq'')) {
         &exit_unknown("UNKNOWN - Line metric '$query' is not currently supported by this plugin.");
      }
   }

   &set_thresholds(\@line_queries,"line");
}

# Sentry4 phase user thresholds
sub sanitize_phase_set_thresholds() {

   # There are certain queries that cannot be mixed with ones that require thresholds. I'm simply 
   # not going to expend the effort to support this.
   foreach my $query (@phase_queries) {
      if ($query =~ /State|Status|VoltageStatus|PowerFactorStatus|Reactance/) {
         &exit_unknown("UNKNOWN - Phase metric '$query' cannot be included when passing thresholds for other metrics on the command line. Sorry. Setup a separate check for it.");
      }
   }
  
   # 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 (@phase_queries) {
      if (! ($query =~ /Voltage|VoltageDeviation|Current|CrestFactor|ActivePower|ApparentPower|PowerFactor|Energy/) && !($query eq'')) {
         &exit_unknown("UNKNOWN - Phase metric '$query' is not currently supported by this plugin.");
      }
   }

   &set_thresholds(\@phase_queries,"phase");
}

# Sentry4 branch user thresholds
sub sanitize_branch_set_thresholds() {

   # There are certain queries that cannot be mixed with ones that require thresholds. I'm simply 
   # not going to expend the effort to support this.
   foreach my $query (@branch_queries) {
      if ($query =~ /State|Status|CurrentStatus/) {
         &exit_unknown("UNKNOWN - Branch metric '$query' cannot be included when passing thresholds for other metrics on the command line. Sorry. Setup a separate check for it.");
      }
   }
  
   # 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 (@branch_queries) {
      if (! ($query =~ /Current|CurrentUtilized|/) && !($query eq'')) {
         &exit_unknown("UNKNOWN - Branch metric '$query' is not currently supported by this plugin.");
      }
   }

   &set_thresholds(\@branch_queries,"branch");
}

#
# SENTRY3 SET THRESHOLDS
#

# Sentry3 set infeed thresholds
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.");
      }
   }

   &set_thresholds(\@infeed_queries,"infeed");
}

# Sentry3 set tower thresholds
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.");
      }
   }

   &set_thresholds(\@tower_queries,"tower");
}
 
#
# SENTRY3/SENTRY4 temp/humid set thresholds
#

# 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) ) ) {

      # This is always zero
      my $temp_index = 0;

      &set_sensor_threshold(\$temp_threshold,$temp_index);

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

      &set_sensor_threshold(\$humid_threshold,$humid_index);

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

      &set_sensor_threshold(\$dewpoint_threshold,$dew_index);

      $user_dewpoint_threshold_set = 1;
   } 
}

sub set_sensor_threshold {

   my ($sensor_threshold,$sensor_index) = @_;

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

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

# 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 2.1

=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 (Sentry3 Products)
 4. Infeeds (Sentry3 Products)
 5. Cords (Sentry4 Products)
 6. Lines (Sentry4 Products)
 7. Phases (Sentry4 Products)
 8. Branches (Sentry4 Products) 

=head2 Environment

 An optional feature of a CDU are temperature and humidity probes. On most units, only two T/H
 ports exist. Some "Link" or "Expansion" CDUs also have T/H ports. When using an EMCU 1-1B even
 more T/H probes are available. This application is designed to support any number of T/H probes
 available to the system. The way these are identified vary between Sentry3 and Sentry4 products.

 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. Unfortunately if any ports have no sensor connected the plugin will return an UNKNOWN state
 indicating that some sensors are notFound. In this case you will need to explicity indicate which
 sensors to query (I have no way of knowing if a sensor isn't really there, or if it failed)
 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 (Sentry3 Products)

 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 (Sentry3 Products)

 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 (Sentry3 Products)

 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 Cords (Sentry4 Products)

 Cord state and statisitcs are checked using the --cord directive. If specificed with no arguments
 only the Status and State metrics of the cord(s) are checked. The ability to query a specific cord
 does not exist at this time. If any other state is encountered for either object a WARNING is 
 generated. 

 There are other status objects that can be queried in addtion to Status and State. Check any number
 of these objects by passing a comma separated list to the --cord directive. They do not accept 
 thresholds. The "normal" state of each metric is hard-coded (usually either "normal" or "on"). Here 
 is the full list of available "State" metrics:
 
 State
 Status
 ActivePowerStatus
 ApparentPowerStatus
 PowerFactorStatus
 OutOfBalanceStatus

 Other non-state metrics can be queried in the same way, but require a threshold. At this time these
 "metered" metrics do not honor any of the built-in thresholds available on the CDU. If you look in 
 the code, I am collecting any available Warning/Alarm metrics, but I have not coded in the ability to
 use them. This is planned in a future version, I hope. If a metric is not available for some reason,
 the plugin will identify this. Here are the cord metrics:

 PowerCapacity
 ActivePower
 ApparentPower
 PowerUtilized
 PowerFactor
 Energy
 Frequency
 OutOfBalance

 It is very important to note that the 'Status' checks are largely skipped when querying any of these
 metrics. 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 '--cord', or by specifying the "Status" checks explicity. 

 The naming convention of the cords is very similar (identical) to how all the other resources are 
 identified in the system.

 Thresholds are applied the same way as is in other checks. ORDER DOES MATTER. The order in which the
 metrics are listed is the order in which the thresholds should be "chained". See the THRESHOLDS section
 for specifics.

 Here are some examples:

 $ check_cdu.pl -H 192.168.0.1 -C public --cord
 OK - BLDG_ROOM_RACK, Master_Cord_A(AA) Status: normal(0) State: on(1), Link1_Cord_A(BA) \
 Status: normal(0) State: on(1)

 $ check_cdu.pl -H 192.168.0.1 -C public --cord ActivePowerStatus,OutOfBalanceStatus
 OK - BLDG_ROOM_RACK, Master_Cord_A(AA) ActivePowerStatus: normal(0), Master_Cord_A(AA) \
 OutOfBalanceStatus: normal(0), Link1_Cord_A(BA) ActivePowerStatus: normal(0), Link1_Cord_A(BA) \
 OutOfBalanceStatus: normal(0)

 $ check_cdu.pl -H 192.168.0.1 -C public --cord ActivePower,PowerUtilized --warning 2500,20 --critical 4000,50
 OK - BLDG_ROOM_RACK, Master_Cord_A(AA) ActivePower: 1442W, Master_Cord_A(AA) PowerUtilized: 8.1%, \
 Link1_Cord_A(BA) ActivePower: 1511W, Link1_Cord_A(BA) PowerUtilized: 8.2%

 $ check_cdu.pl -H 192.168.0.1 -C public --cord PowerCapacity
 WARNING - BLDG_ROOM_RACK, Link1_Cord_A(BA) PowerCapacity: -1VA

 (This is a very bizarre but interesting scenario. I included PowerCapacity because it was there, but
 who would logically check a static value such as the capacity of a cord? 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 Lines (Sentry4 Products)

 Line state and statisitcs are checked using the -line directive. If specificed with no arguments
 only the Status and State metrics of the cord(s) are checked. The ability to query a specific cord
 does not exist at this time. If any other state is encountered for either object a WARNING is 
 generated. 

 There are other status objects that can be queried in addtion to Status and State. Check any number
 of these objects by passing a comma separated list to the --line directive. They do not accept 
 thresholds. The "normal" state of each metric is hard-coded (usually either "normal" or "on"). Here 
 is the full list of available "State" metrics:
 
 State
 Status
 CurrentStatus

 Other non-state metrics can be queried in the same way, but require a threshold. At this time these
 "metered" metrics do not honor any of the built-in thresholds available on the CDU. If you look in 
 the code, I am collecting any available Warning/Alarm metrics, but I have not coded in the ability to
 use them. This is planned in a future version, I hope. If a metric is not available for some reason,
 the plugin will identify this. Here are the line metrics:

 CurrentCapacity
 Current
 CurrentUtilized

 It is very important to note that the 'Status' checks are largely skipped when querying any of these
 metrics. 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 '--line', or by specifying the "Status" checks explicity. 

 The naming convention of the cords is very similar (identical) to how all the other resources are 
 identified in the system.

 Thresholds are applied the same way as is in other checks. ORDER DOES MATTER. The order in which the
 metrics are listed is the order in which the thresholds should be "chained". See the THRESHOLDS section
 for specifics.

 Here are some examples:

 $ check_cdu.pl -H 192.168.0.1 -C public --line
 OK - BLDG_ROOM_RACK, AA:L1(AA1) Status: normal(0) State: on(1), AA:L2(AA2) Status: normal(0) \
 State: on(1), AA:L3(AA3) Status: normal(0) State: on(1), AA:N(AA4) Status: normal(0) State: on(1), \
 BA:L1(BA1) Status: normal(0) State: on(1), BA:L2(BA2) Status: normal(0) State: on(1), BA:L3(BA3) \
 Status: normal(0) State: on(1), BA:N(BA4) Status: normal(0) State: on(1)

 $ check_cdu.pl -H 192.168.0.1 -C public --line CurrenStatus
 OK - BLDG_ROOM_RACK, AA:L1(AA1) CurrentStatus: normal(0), AA:L2(AA2) CurrentStatus: normal(0), \
 AA:L3(AA3) CurrentStatus: normal(0), AA:N(AA4) CurrentStatus: normal(0), BA:L1(BA1) CurrentStatus: \
 normal(0), BA:L2(BA2) CurrentStatus: normal(0), BA:L3(BA3) CurrentStatus: normal(0), BA:N(BA4) \
 CurrentStatus: normal(0)

 $ check_cdu.pl -H 192.168.0.1 -C public --line Current,CurrentUtilized --warning 5,40 --critical 10,95
 OK - BLDG_ROOM_RACK, AA:L1(AA1) Current: 3.06A, AA:L1(AA1) CurrentUtilized: 9.5%, AA:L2(AA2) \
 Current: 2.23A, AA:L2(AA2) CurrentUtilized: 6.9%, AA:L3(AA3) Current: 2.1A, AA:L3(AA3) \
 CurrentUtilized: 6.5%, AA:N(AA4) Current: 1.05A, AA:N(AA4) CurrentUtilized: 3.2%, BA:L1(BA1) \
 Current: 3.18A, BA:L1(BA1) CurrentUtilized: 9.9%, BA:L2(BA2) Current: 2.36A, BA:L2(BA2) \
 CurrentUtilized: 7.3%, BA:L3(BA3) Current: 2.07A, BA:L3(BA3) CurrentUtilized: 6.4%, BA:N(BA4) \
 Current: 1.1A, BA:N(BA4) CurrentUtilized: 3.4%

=head2 Phases (Sentry4 Products)

 Read the documentation for Cords and Lines. Phases are handled the same way.

 Available "State" metrics:

 State
 Status
 VoltageStatus
 PowerFactorStatus
 Reactance

 Metered Metrics:

 Voltage
 VoltageDeviation
 Current
 CrestFactor
 ActivePower
 ApparentPower
 PowerFactor
 Energy

 NOTE: Reactance is evaluated in terms of the following states:

 unknown
 capacitive
 inductive
 resistive

 I opted to choose "capacitive" as the "OK" state. This could really not work well. YMMV 

 PowerFactor readings of "-0.01" are essentially ignored and treated as "OK" regardless of what 
 thresholds are in use. It was impossible to set logical thresholds for a Power Factor while also
 accounting for "-0.01" which is reported from very lightly (or unloaded) PDUs. 

=head2 Branches (Sentry4 Products)

 Read the documentation for Cords and Lines. Branches are handled the same way.

 Available "State" metrics:

 State
 Status
 CurrentStatus

 Metered Metrics:

 CurrentCapacity
 Current
 CurrentUtilized

=head2 Contact Sensors

 Contact Closure sensors (Dry Contacts) are available when the EMCU-1-1B unit is used. Each firmware
 version and even each CDU type can enumerate the sensors differently, so the IDs have been "simplified"
 for use in this plugin. Do not use E1, C1, etc as the ID. Just use 1-4. The plugin figures the rest
 out automagically. A state/status of "normal(0)" returns an OK. Anything else returns a WARNING. I 
 didn't bother to make this configurable, but you can hack the code yourself to change this if you want.
 If you don't explicity specify which IDs to query, the script looks at all four of them. 
 
 $ check_cdu.pl -H 192.168.0.1 -C public --contact 1,2
 OK - BLDG_ROOM_RACK, FRONT_DOOR(B1): normal(0), REAR_DOOR(B2): normal(0)

=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.

