#!/usr/bin/perl -w

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

# 20171106: Added functionality for SensorProbe Plus units.

use Net::SNMP;
use Getopt::Std;
use Getopt::Long;
use Nagios::Plugin::Threshold;
use List::MoreUtils qw(uniq);
use Data::Dumper;
use strict;
use Switch;

# 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 ($debug, $timeout, $port, $help, $community, $host, $oksummary, $warning_string, $critical_string, $fahrenheit, $celsius, $temp, $humid, $list, $dewtemp, $dewdelta, $ths, $version);
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, @tempsensors, @humidsensors, @sensors, @ths_list);
my (%sensor_data);
# For global function
my ($spStatus, $spManufName, $spProductName, $sysDescr, $sysContact, $sysName, $sysLocation);
my $akcp_model = 1;

#
# 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,
      "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,
      "list"		=>	\$list,
      "dewtemp"		=>	\$dewtemp,
      "dewdelta"	=>	\$dewdelta,
      "ths=s"		=>	\$ths,
      "debug"		=>	\$debug,
      "version"		=>	\$version
      );

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

if ($help) {
   usage();
}

if ($version) {
   version();
}

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

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

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


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

# Fire up an SNMP session
my ($session, $error) = Net::SNMP->session (
      hostname     =>      $host,
      community    =>      $community,
      version      =>      '1',
      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);
      }

      # This is simple verification to make sure an AKCP port is specified.
      #foreach my $ths (@ths_list) {
      #   if (! ($ths =~ /[0-9]+/) || length($ths) > 1) {
      #      &exit_unknown("UNKNOWN - Basic sensor ID verification failed for '$ths'. If this an error, read the docs, change the regex");
      #   }
      #}
   } 
   # This is OK, we will query all sensors on the AKCP.
   # If any of the sensors are offline, it will generate an error.
   #else {
   #   &exit_unknown("UNKNOWN - No sensors specified to query");
   #}

   &sanitize_set_user_thresholds;
   &build_sensor_data;

   if ($list) {
      &list_sensors;
      exit(0);
   }

   &process_sensor_data;
}

# Nothing but host and community was provided on the command line. The plugin will simply look at the
# only available generic metric, which is spStatus. The MIB defines this as:
# "The current status of this sensorProbe. The status of the sensorProbe is the worst status of all 
# the sensors under the control of this sensorProbe."

else {
   my $msg_base = "$sysLocation $sysName ";

   switch ($spStatus) {
      case 1 { 
         my $msg_txt = $msg_base . "AKCP Overall Status: noStatus(1)";
         push(@WARNING,$msg_txt);
      }
      case 2 { 
         my $msg_txt = $msg_base . "AKCP Overall Status: normal(2)";
         push(@OK,$msg_txt);
      }
      case 3 {
         my $msg_txt = $msg_base . "AKCP Overall Status: warning(3)";
         push(@WARNING,$msg_txt);
      }
      case 4 {
         my $msg_txt = $msg_base . "AKCP Overall Status: critical(4)";
         push(@CRITICAL,$msg_txt);
      }
      case 5 {
         my $msg_txt = $msg_base . "AKCP Overall Status: sensorError(5)";
         push(@WARNING,$msg_txt);
      }
      else {
         my $msg_txt = $msg_base . "AKCP Overall Status: outOfMIBRange - seek help";
         push(@WARNING,$msg_txt);
      }
   }
}

### 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 "$SCRIPT_NAME -H <host> -C <community> [--timeout SNMP timeout] [--port 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. 0,1,2)\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: K - securityProbe 5ES SEC-MX25V405a May 14 2014 16:09:09, Location: BLDG ROOM, 3 metrics are OK\n";
   print "\t   Without: OK - East MAT(0) Delta: 71.69F, West MAT(1) Delta: 70.02F, Space(2) Delta: 57.25F\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--list\n";
   print "\t   List available sensors to query on the AKCP. SPX units have tricky IDs! Use with --temp or --humid\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--help\n";
   print "\t   This message. Please issue a 'perldoc $SCRIPT_NAME' for the full documentation.\n\n";
   print "\t--debug\n";
   print "\t   Print useful debugging information (mostly development purposes only)\n\n";
   print "\t--version\n";
   print "\t   Print version information.\n\n\n";
   print "EXAMPLES FOR THE IMPATIENT PEOPLE:\n";
   print "\$ $SCRIPT_NAME -H 192.168.0.1 -C public --temp --humid --fahrenheit --ths 0,1,2 --warning 60:85,40:60 --critical 95,15:\n";
   print "\$ $SCRIPT_NAME -H 192.168.0.1 -C public --dewdelta --warning 2: --critical 1: --ths 0,1,2 --fahrenheit --oksummary\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) 2015 Eric Schoeller (eric.schoeller <at> coloradoDOTedu).
   All rights reserved.

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

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

EOF
exit 3;

}


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

#
# Temperature conversion functions
#

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

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

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

#
# Helper functions for t_dewpoint
#

#
# Estimate the actual water vapor pressure P_a
#

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

#
# Estimate for gamma(t,rh)
#

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

## End dewpoint code.

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

sub global {

   my $akcp_model_object = $session->get_request( varbindlist => ['.1.3.6.1.4.1.3854.1.2.2.1.109.0'],);
   if (!(defined $akcp_model_object)) { &exit_unknown("UNKNOWN - Cannot determine AKCP model. SNMP ERROR: " . $session->error()) }

   $akcp_model = $akcp_model_object->{'.1.3.6.1.4.1.3854.1.2.2.1.109.0'};
   # SensorProbePlus is model '16' and uses a different spTable object.
   if ($akcp_model == 16) {

      # Set inititally to no status
      $spStatus = 1;

      # hard-coding these since they don't appear to be available anymore.
      $spManufName = "AKCP";
      $spProductName = "SensorProbe Plus";

      # Grab the commonStatus table which lists states of all connected sensors. The worst of the lot
      # will be considered the overall status of the AKCP. I think this is kinda what older models 
      # were actually doing?
      my $commonStatusTable = $session->get_table( baseoid => '.1.3.6.1.4.1.3854.3.5.1.1.6');
      while (my ($key, $value) = each %{$commonStatusTable}) {
         if ($value > $spStatus) {
            $spStatus = $value;
         }
      }    
   }
    
   else {
      # Fetch generic system information via SNMP
      my $spTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.3854.1.1');

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

      $spStatus = $spTable->{'.1.3.6.1.4.1.3854.1.1.2.0'}; #INTEGER: critical(4)
      $spManufName = $spTable->{'.1.3.6.1.4.1.3854.1.1.6.0'}; #STRING: "AKCP"
      $spProductName = $spTable->{'.1.3.6.1.4.1.3854.1.1.8.0'}; #STRING: "sensorProbe8 v 2.0"
   }


   my $sysTable  = $session->get_table( baseoid   => '.1.3.6.1.2.1.1');
   
   if (!defined $sysTable) {
      $error = $session->error();
      &exit_unknown("UNKNOWN - SNMP ERROR: $error");
   }
   $sysDescr = $sysTable->{'.1.3.6.1.2.1.1.1.0'}; #STRING: sensorProbe8 v 2.0 SP8459i 260612
   $sysContact = $sysTable->{'.1.3.6.1.2.1.1.4.0'}; #STRING: xyz@colorado.edu
   $sysName = $sysTable->{'.1.3.6.1.2.1.1.5.0'}; #STRING: BLDG-ROOM-akcp
   $sysLocation = $sysTable->{'.1.3.6.1.2.1.1.6.0'}; # STRING: BLDG ROOM 
   chomp($sysDescr);

   if ($debug) { print "spStatus:$spStatus spManufName:$spManufName spProductName:$spProductName sysDescr:$sysDescr sysContact:$sysContact sysName:$sysName sysLocation:$sysLocation\n" };
}


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

   if ($debug) { print "Building Sensor Data ...\n" };

   # Get just the list of sensors on the AKCP.
   if ($temp || $dewtemp || $dewdelta) {
      my ($tempSensor, $tempTable);
      if ($debug) { print "Getting Temperature Data ...\n" };
      if ($akcp_model==16) { $tempSensor = $session->get_table( baseoid => '.1.3.6.1.4.1.3854.3.5.2.1.2'); } 
      else { $tempSensor = $session->get_table( baseoid => '.1.3.6.1.4.1.3854.1.2.2.1.16.1.1'); }

      if (!defined $tempSensor) {
         $error = $session->error();
         &exit_unknown("UNKNOWN - Cannot fetch Temperature Sensor Description table SNMP ERROR: $error");
      }
      
      # Get the entire table.
      if ($akcp_model==16) { $tempTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.3854.3.5.2.1'); }
      else { $tempTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.3854.1.2.2.1.16'); }

      if (!defined $tempTable) {
         $error = $session->error();
         &exit_unknown("UNKNOWN - Cannot fetch Temperature Sensor table SNMP ERROR: $error");
      }
      
      # Find the child OID suffix of each sensor.
      foreach my $key (sort (keys %{$tempSensor})) {

         my $sensor_id;
         if ($akcp_model==16) { $key =~ /.1.3.6.1.4.1.3854.3.5.2.1.2(.*)/; $sensor_id=$1; }
         else { $key =~ /.1.3.6.1.4.1.3854.1.2.2.1.16.1.1(.*)/; $sensor_id=$1; }
         push(@tempsensors, $sensor_id);
         if ($debug) { print "Found Temp sensors: @tempsensors\n" };
      }   
      foreach my $sensor (@tempsensors) {
     
         # Strip the leading decimal from the sensor ID
         ($sensor_data{$sensor}->{'ID'} = $sensor) =~ s/^\.//;
         # old way - not SPX compatible
         #$sensor_data{$sensor}->{'ID'} = substr($sensor,-1);

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

            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.1' . $sensor) ) { $sensor_data{$sensor}->{'TempDescription'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.2' . $sensor) ) { $sensor_data{$sensor}->{'TempDescription'} = $value } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.3' . $sensor) ) { $sensor_data{$sensor}->{'TempDegree'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.4' . $sensor) ) { $sensor_data{$sensor}->{'TempDegree'} = $value } # SPX

            if (($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.4' . $sensor)) || ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.6' . $sensor)) ) {  # both
               my @val_array = qw(notValid noStatus normal highWarning highCritical lowWarning lowCritical sensorError);
               $sensor_data{$sensor}->{'TempStatus'} = &translate(\@val_array, $value);
            }

            if (($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.5' . $sensor)) || ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.8' . $sensor)) ) {  # both
               my @val_array = qw(notValid online offline);
               $sensor_data{$sensor}->{'TempOnline'} = &translate(\@val_array, $value);
            }

            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.7' . $sensor) ) { $sensor_data{$sensor}->{'TempHighWarning'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.11' . $sensor) ) { $sensor_data{$sensor}->{'TempHighWarning'} = ($value/10) } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.8' . $sensor) ) { $sensor_data{$sensor}->{'TempHighCritical'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.12' . $sensor) ) { $sensor_data{$sensor}->{'TempHighCritical'} = ($value/10) } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.9' . $sensor) ) { $sensor_data{$sensor}->{'TempLowWarning'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.10' . $sensor) ) { $sensor_data{$sensor}->{'TempLowWarning'} = ($value/10) } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.10' . $sensor) ) { $sensor_data{$sensor}->{'TempLowCritical'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.9' . $sensor) ) { $sensor_data{$sensor}->{'TempLowCritical'} = ($value/10) } # SPX

            if (($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.16.1.12' . $sensor)) || ($key eq ('.1.3.6.1.4.1.3854.3.5.2.1.5' . $sensor)) ) {  # both
               if ($akcp_model==16) {
                  if ($value eq 'F') { $sensor_data{$sensor}->{'TempDegreeType'} = 'fahr(0)' ; }
                  elsif ($value eq 'C') { $sensor_data{$sensor}->{'TempDegreeType'} = 'celsius(1)' ; }
                  else { $sensor_data{$sensor}->{'TempDegreeType'} = 'translateError' ;}
               }
               else {
                  my @val_array = qw(fahr celsius);
                  $sensor_data{$sensor}->{'TempDegreeType'} = &translate(\@val_array, $value);
               }
            }

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

   if ($humid || $dewtemp || $dewdelta) {
      my ($humidSensor, $humidTable);
      if ($debug) { print "Getting Humidity Data ...\n" };
      if ($akcp_model==16) { $humidSensor = $session->get_table( baseoid => '.1.3.6.1.4.1.3854.3.5.3.1.2'); }
      else { $humidSensor = $session->get_table( baseoid => '.1.3.6.1.4.1.3854.1.2.2.1.17.1.1'); }

      if (!defined $humidSensor) {
         $error = $session->error();
         &exit_unknown("UNKNOWN - SNMP ERROR: $error");
      }
 
      if ($akcp_model==16) { $humidTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.3854.3.5.3'); }
      else { $humidTable = $session->get_table( baseoid   => '.1.3.6.1.4.1.3854.1.2.2.1.17'); }

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

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

         my $sensor_id;
         if ($akcp_model==16) { $key =~ /.1.3.6.1.4.1.3854.3.5.3.1.2(.*)/; $sensor_id=$1; }
         else { $key =~ /.1.3.6.1.4.1.3854.1.2.2.1.17.1.1(.*)/; $sensor_id=$1; }
         push(@humidsensors, $sensor_id);
         if ($debug) { print "Found Temp sensors: @humidsensors\n" };
      }   
      foreach my $sensor (@humidsensors) {
     
         # Fix this to get rid of the decimal point with substr
         #$sensor_data{$sensor}->{'ID'} = substr($sensor,-1);
         # Strip the leading decimal from the sensor ID
         ($sensor_data{$sensor}->{'ID'} = $sensor) =~ s/^\.//;

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

            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.1' . $sensor) ) { $sensor_data{$sensor}->{'HumidityDescription'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.2' . $sensor) ) { $sensor_data{$sensor}->{'HumidityDescription'} = $value } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.3' . $sensor) ) { $sensor_data{$sensor}->{'HumidityPercent'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.4' . $sensor) ) { $sensor_data{$sensor}->{'HumidityPercent'} = $value } # SPX

            if (($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.4' . $sensor)) || ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.6' . $sensor)) ) {  # both
               my @val_array = qw(notValid noStatus normal highWarning highCritical lowWarning lowCritical sensorError);
               $sensor_data{$sensor}->{'HumidityStatus'} = &translate(\@val_array, $value);
            }

            if (($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.5' . $sensor)) || ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.8' . $sensor)) ) { # both
               my @val_array = qw(notValid online offline);
               $sensor_data{$sensor}->{'HumidityOnline'} = &translate(\@val_array, $value);
            }

            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.7' . $sensor) ) { $sensor_data{$sensor}->{'HumidityHighWarning'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.11' . $sensor) ) { $sensor_data{$sensor}->{'HumidityHighWarning'} = ($value/10) } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.8' . $sensor) ) { $sensor_data{$sensor}->{'HumidityHighCritical'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.12' . $sensor) ) { $sensor_data{$sensor}->{'HumidityHighCritical'} = ($value/10) } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.9' . $sensor) ) { $sensor_data{$sensor}->{'HumidityLowWarning'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.10' . $sensor) ) { $sensor_data{$sensor}->{'HumidityLowWarning'} = ($value/10) } # SPX
            if ($key eq ('.1.3.6.1.4.1.3854.1.2.2.1.17.1.10' . $sensor) ) { $sensor_data{$sensor}->{'HumidityLowCritical'} = $value }
            if ($key eq ('.1.3.6.1.4.1.3854.3.5.3.1.9' . $sensor) ) { $sensor_data{$sensor}->{'HumidityLowCritical'} = ($value/10) } # SPX

         }
      }
   }

   # Rebuild a single @sensors array for use in the rest of the program.
   # We aren't supporting checking humidity of one sensor, temp of another.
   # You need to invoke the check twice to do that.
   if (($temp && $humid) || $dewdelta || $dewtemp) {
      my @usensors;
      push(@usensors,@tempsensors);
      push(@usensors,@humidsensors);
      @sensors = uniq @usensors;
   }
   elsif ($temp) {
      @sensors = @tempsensors;
   }
   elsif ($humid) {
      @sensors = @humidsensors;
   } 

   if ($debug) { print "Sensor Data:\n" };
   if ($debug) { print Dumper %sensor_data };

}


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

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

}

sub list_sensors() {
 
   print "ID: Name\n";
   for my $ID ( sort keys %sensor_data ) {
      if ($temp) {
         print "$sensor_data{$ID}{ID}: $sensor_data{$ID}{TempDescription}\n";
      }
      if ($humid) {
         print "$sensor_data{$ID}{ID}: $sensor_data{$ID}{HumidityDescription}\n";
      }
   }
}

# 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;
   if (@ths_list) {
      foreach my $thsq (@ths_list) {
         foreach my $sensor (@sensors) {
            if (($temp || $dewtemp || $dewdelta) && ($thsq eq $sensor_data{$sensor}->{'ID'})) {
               if ($sensor_data{$sensor}->{'TempOnline'} =~  /online/) {
                  if ($debug) { print "proceeding with valid temp sensor: $sensor_data{$sensor}->{'ID'} $sensor_data{$sensor}->{'TempOnline'}\n" };
                  push(@sensors_to_query,$sensor);
               }
               if ($sensor_data{$sensor}->{'TempOnline'} =~  /offline/) {
                  if ($debug) { print "sensor is offline: $sensor_data{$sensor}->{'ID'} $sensor_data{$sensor}->{'TempOnline'}\n" };
                  push(@WARNING,"Temperature Sensor $sensor_data{$sensor}->{'TempDescription'}($sensor_data{$sensor}->{'ID'}) 
                   $sensor_data{$sensor}->{'TempOnline'} $sensor_data{$sensor}->{'TempStatus'}");
               }
            }   

            if (($humid || $dewtemp || $dewdelta) && ($thsq eq $sensor_data{$sensor}->{'ID'})) {
               if ($sensor_data{$sensor}->{'HumidityOnline'} =~  /online/) {
                  if ($debug) { print "proceeding with valid humid sensor: $sensor_data{$sensor}->{'ID'} $sensor_data{$sensor}->{'HumidityOnline'}\n" };
                  push(@sensors_to_query,$sensor);
               }
               if ($sensor_data{$sensor}->{'HumidityOnline'} =~  /offline/) {
                  if ($debug) { print "sensor is offline: $sensor_data{$sensor}->{'ID'} $sensor_data{$sensor}->{'HumidityOnline'}\n" };
                  push(@WARNING,"Humidity Sensor $sensor_data{$sensor}->{'HumidityDescription'}($sensor_data{$sensor}->{'ID'}) 
                   $sensor_data{$sensor}->{'HumidityOnline'} $sensor_data{$sensor}->{'HumidityStatus'}");
               }
            }
         }
      }

      # Remote any duplicates. This is common when humid and temp are both defined. 
      @sensors_to_query = uniq(@sensors_to_query);
      if (!(@sensors_to_query)) {
         &exit_unknown("UNKNOWN - Did not find any sensors to query. Use the --list option to find the correct IDs");
      } 
   }

   # No $ths was passed at the command line, check all sensors.
   else {
      @sensors_to_query = @sensors;
      # Make sure the sensor(s) are online. Remove them from the list otherwise.
      foreach my $sensor (@sensors) {
         if ($temp && $sensor_data{$sensor}->{'TempOnline'} =~  /offline/) {
            # Be careful changing this to a push to @WARNING or @CRITICAL. If the rest of the code
            # proceeds with a sensor that is offline I doubt that I have handled those exceptions 
            # properly because the AKCP will likely return data that I'm not equipped to handle.
            push(@WARNING,"Temperature Sensor $sensor_data{$sensor}->{'TempDescription'}($sensor_data{$sensor}->{'ID'}) $sensor_data{$sensor}->{'TempOnline'}");
            # Hack-ish array removal code
            my $index = 0;
            $index++ until $sensors_to_query[$index] eq $sensor;
            splice(@sensors_to_query, $index, 1);
         }
         if ($humid && $sensor_data{$sensor}->{'HumidityOnline'} =~  /offline/) {
            # Be careful changing this to a push to @WARNING or @CRITICAL. If the rest of the code
            # proceeds with a sensor that is offline I doubt that I have handled those exceptions 
            # properly because the AKCP will likely return data that I'm not equipped to handle.
            push(@WARNING,"Humidity Sensor $sensor_data{$sensor}->{'HumidityDescription'}($sensor_data{$sensor}->{'ID'}) $sensor_data{$sensor}->{'HumidityOnline'}");
            my $index = 0;
            $index++ until $sensors_to_query[$index] eq $sensor;
            splice(@sensors_to_query, $index, 1);
         }
      }
   } 

   foreach my $sensor (@sensors_to_query) {

      my ($TempLowWarning, $TempHighWarning, $TempLowCritical, $TempHighCritical, $TempDegreeRaw, $Temp, $temp_warning_string, $temp_critical_string, 
       $humid_warning_string, $humid_critical_string, $TempUnit, $DewValue, $DewUnit, $dewpoint);

      # Base text to use in all messages
      my ($t_msg_base, $h_msg_base);
      if ($temp || $dewtemp || $dewdelta) { $t_msg_base = $sensor_data{$sensor}->{'TempDescription'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . ": " };
      if ($humid || $dewtemp || $dewdelta) { $h_msg_base = $sensor_data{$sensor}->{'HumidityDescription'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . ": " };

      if ($temp || $dewtemp || $dewdelta) {
         # 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}->{'TempDegreeType'} =~ /celsius/) ) {
            if ($debug) { print "Switching celsius readings to fahrenheit\n"};
            $TempLowWarning = t_convert_c_to_f($sensor_data{$sensor}->{'TempLowWarning'});
            $TempHighWarning = t_convert_c_to_f($sensor_data{$sensor}->{'TempHighWarning'});
            $TempLowCritical = t_convert_c_to_f($sensor_data{$sensor}->{'TempLowCritical'});
            $TempHighCritical = t_convert_c_to_f($sensor_data{$sensor}->{'TempHighCritical'});
            $TempDegreeRaw = t_convert_c_to_f($sensor_data{$sensor}->{'TempDegreeRaw'});
            $Temp = t_convert_c_to_f($sensor_data{$sensor}->{'TempDegree'});
            $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}->{'TempDegreeType'} =~ /fahr/) ) {
            if ($debug) { print "switching fahrenheit readings to celsius\n"};
            $TempLowWarning = t_convert_f_to_c($sensor_data{$sensor}->{'TempLowWarning'});
            $TempHighWarning = t_convert_f_to_c($sensor_data{$sensor}->{'TempHighWarning'});
            $TempLowCritical = t_convert_f_to_c($sensor_data{$sensor}->{'TempLowCritical'});
            $TempHighCritical = t_convert_f_to_c($sensor_data{$sensor}->{'TempHighCritical'});
            $TempDegreeRaw = t_convert_f_to_c($sensor_data{$sensor}->{'TempDegreeRaw'});
            $Temp = t_convert_f_to_c($sensor_data{$sensor}->{'TempDegree'});
            $TempUnit ="C";
         }

         # The requested temperature scale matches the configured temperature scale.
         else {
            if ($debug) { print "readings and scale both configured the same\n"}; 
            $TempLowWarning = $sensor_data{$sensor}->{'TempLowWarning'};
            $TempHighWarning = $sensor_data{$sensor}->{'TempHighWarning'};
            $TempLowCritical = $sensor_data{$sensor}->{'TempLowCritical'};
            $TempHighCritical = $sensor_data{$sensor}->{'TempHighCritical'};
            $TempDegreeRaw = $sensor_data{$sensor}->{'TempDegreeRaw'};
            $Temp = $sensor_data{$sensor}->{'TempDegree'};

            if ($sensor_data{$sensor}->{'TempDegreeType'} =~ /celsius/) {
               $TempUnit ="C";
            }
            if ($sensor_data{$sensor}->{'TempDegreeType'} =~ /fahr/) {
               $TempUnit ="F";
            }

         } 
      }
   
      # If a threshold was not defined on the command line, set them automatically here based
      # on the thresholds set on the unit. AKCP has both a WARNING and CRITICAL typically so
      # they are both set here.
      if ( !defined($user_temp_threshold_set) && defined($temp)) {
         if ($debug) { print "using AKCP defined temperature thresholds, nothing provided on command line\n"};
         $temp_warning_string = $TempLowWarning . ":" . $TempHighWarning;
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds( warning => $temp_warning_string);
         $temp_critical_string = $TempLowCritical . ":" . $TempHighCritical;
         $temp_threshold = Nagios::Plugin::Threshold->set_thresholds( critical => $temp_critical_string);
      }

      if ( !defined($user_humid_threshold_set) && defined($humid)) {
         if ($debug) { print "using AKCP defined humidity thresholds, nothing provided on command line\n"};
         $humid_warning_string = $sensor_data{$sensor}->{'HumidityLowWarning'} . ":" . $sensor_data{$sensor}->{'HumidityHighWarning'};
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds( warning => $humid_warning_string);
         $humid_critical_string = $sensor_data{$sensor}->{'HumidityLowCritical'} . ":" . $sensor_data{$sensor}->{'HumidityHighCritical'};
         $humid_threshold = Nagios::Plugin::Threshold->set_thresholds( critical => $humid_critical_string);
      } 

      # The AKCP 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");
      }
         
      my ($temp_status, $humid_status);

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

         # Use the Nagios::Plugin magic to determine the status of each sensor given the thresholds we have constructed.
         $temp_status = $temp_threshold->get_status($TempDegreeRaw);

         # If the sensor is in an Error state, handle that now. However, I don't believe this code
         # will actually ever get used. We look for this, and handle it, earlier on in the code execution.
         if ($sensor_data{$sensor}->{'TempOnline'} =~ /offline/) {
            if ($debug) { print "Sensor reported offline\n"};
            my $msg_txt = $t_msg_base . $sensor_data{$sensor}->{'TempOnline'};
            push(@UNKNOWN, $msg_txt);
         }

         # The sensor is most likely in normal(0) state, so report things as normal.
         else {
            my $msg_txt = $t_msg_base . sprintf("%.2f",$TempDegreeRaw) . $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)) {
         
         # Use the Nagios::Plugin magic to determine the status of each sensor given the thresholds we have constructed.
         $humid_status = $humid_threshold->get_status($sensor_data{$sensor}->{'HumidityPercent'});

         # Same as above, if the sensor is in Error, report it differently.
         if ($sensor_data{$sensor}->{'HumidityOnline'} =~ /offline/) {
            if ($debug) { print "Sensor reported offline\n"};
            my $msg_txt = $h_msg_base . $sensor_data{$sensor}->{'HumidityOnline'};
            push(@UNKNOWN, $msg_txt);
         }

         # Again, report as normal if the sensor is not in error.
         else {
            my $msg_txt = $h_msg_base . $sensor_data{$sensor}->{'HumidityPercent'} . "%";
            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}->{'HumidityOnline'} =~ /offline/) {
            my $msg_txt = $h_msg_base . $sensor_data{$sensor}->{'HumidityOnline'};
            push(@UNKNOWN, $msg_txt);
         }

         elsif ($sensor_data{$sensor}->{'TempOnline'} =~ /offline/) {
            my $msg_txt = $t_msg_base . $sensor_data{$sensor}->{'TempOnline'};
            push(@UNKNOWN, $msg_txt);
         }

         else {
            # I guess this is where the magic happens.
            if ($sensor_data{$sensor}->{'TempDegreeType'} =~ /celsius/) { 
               $dewpoint = t_dewpoint($sensor_data{$sensor}->{'TempDegreeRaw'}, $sensor_data{$sensor}->{'HumidityPercent'});
            }
            elsif ($sensor_data{$sensor}->{'TempDegreeType'} =~ /fahr/) {
               $dewpoint = t_dewpoint(t_convert_f_to_c($sensor_data{$sensor}->{'TempDegreeRaw'}), $sensor_data{$sensor}->{'HumidityPercent'});
            }
            # Should never happen, but just in case. 
            else {
               my $msg_txt = $t_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}->{'TempDegreeType'} =~ /celsius/) ) {
               $DewValue = t_convert_c_to_f($dewpoint);
               $TempDegreeRaw = t_convert_c_to_f($sensor_data{$sensor}->{'TempDegreeRaw'});
               $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}->{'TempDegreeType'} =~ /fahr/) ) {
               $DewValue = t_convert_f_to_c($dewpoint);
               $TempDegreeRaw = t_convert_f_to_c($sensor_data{$sensor}->{'TempDegreeRaw'});
               $DewUnit ="C";
            }

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

            my $dewpoint_status;
            if (defined($dewtemp)) {
               $dewpoint_status = $dewpoint_threshold->get_status($DewValue);
               my $msg_txt = $sensor_data{$sensor}->{'TempDescription'} . "(" . $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 $TempDegreeRawe-$DewValue
               # directly get_status();
               $dewpoint_status = $dewpoint_threshold->get_status($TempDegreeRaw-$DewValue);
               my $msg_txt = $sensor_data{$sensor}->{'TempDescription'} . "(" . $sensor_data{$sensor}->{'ID'} . ")" . " Delta: ". sprintf("%.2f",($TempDegreeRaw-$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) }
            }
         }
      }
   }
}


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

   if ($debug) { print "Sanitizing user thresholds ...\n" };
    
   # Keep track of the number of metrics being queried. 
   my $num_metrics = 0;
   $num_metrics++ if (defined($temp));
   $num_metrics++ if (defined($humid));
   $num_metrics++ if (defined($dewtemp));
   $num_metrics++ if (defined($dewdelta));
   

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

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

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


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

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

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

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

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

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

      $user_temp_threshold_set = 1;
   }

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

      my $humid_index;

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

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

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

      $user_humid_threshold_set = 1;
   } 

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

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

      $user_dewpoint_threshold_set = 1;
   } 
}


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

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

   if (@WARNING) {
      print "WARNING - ";
      foreach(@WARNING) {
         if (\$_ == \$WARNING[-1]) {
            print "$_";
         }
         else { 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 - ";
      foreach(@UNKNOWN) {
         if (\$_ == \$UNKNOWN[-1]) {
            print "$_";
         }
         else { print "$_, " }
      }
      print "\n";
      exit(3);
   }

   if (@OK) {
      if (defined($oksummary)) {
         print "OK - $sysDescr, Location: $sysLocation, " . scalar(@OK) . " metrics are OK";
      }
      else {
         print "OK - ";
         foreach(@OK) {
            if (\$_ == \$OK[-1]) {
               print "$_";
            }
            else { print "$_, " }
         }
      }
      print "\n";
      exit(0);
   }

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

}



__END__

=head1 NAME

check_akcp2 - Check temperature, humidity and dewpoint from an AKCP device.

=head1 VERSION

This documentation refers to check_akcp2 version 1.0

=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_akcp2.pl -H <host> -C <community> [--timeout SNMP timeout] [--port 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_akcp2 is intended to provide extremely flexible and extensive monitoring support for 
 AKCess Pro networked environmental monitoring solutions. 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 AKCP
 5. Exit appropriately given the status results

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

=head2 Environment

 A popular sensor used on an AKCP device is either a temperature or temperature/humidity probe.
 This plugin supports any number of these probes on just about any type of AKCP device.

 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 WARNING return:
 
 offline(2)

 This applies to both temperature and humidity. When a sensor is offline(2) there are several
 additional status items that may be reported, including:

  noStatus(1),
  normal(2),
  highWarning(3),
  highCritical(4),
  lowWarning(5),
  lowCritical(6),
  sensorError(7)
 
 In its simplest form, the environment checks will query all available T/H probes connected to the
 system. The AKCP 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.
 A basic invocation would resemble:

 $ check_akcp2.pl -H 192.168.0.1 -C public --temp --humid
 OK - N180D East MAT(0): 78.20F, N180D East MAT(0): 42%, N180B West MAT(1): 77.00F, \
 N180B West MAT(1): 44%, N180 Space(2): 67.60F, N180 Space(2): 70%
 
 The various objects queried are returned in a comman 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 TempDegreeType object provided via SNMP. For instances 
 where the AKCP 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_akcp2.pl -H 192.168.0.1 -C public --temp --humid --celsius
 OK - N180D East MAT(0): 25.50C, N180D East MAT(0): 40%, N180B West MAT(1): 24.78C, N180B West MAT(1): 43%

 --fahrenheit 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 WARNING 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 0,1,2 --fahrenheit
 OK - N180D East MAT(0): 77.70F, N180B West MAT(1): 76.60F, N180 Space(2): 67.10F

 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 AKCP 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_akcp2.pl -H 192.168.0.1 -C public --temp --fahrenheit --ths 0,1 --warning 60:80
 OK - N180D East MAT(0): 77.70F, N180B West MAT(1): 76.60F

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

 $ check_akcp2.pl -H 192.168.0.1 -C public --humid --ths 0,1 --warning 10:70
 OK - N180D East MAT(0): 42%, N180B West MAT(1): 43%

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

 $ check_akcp2.pl -H 192.168.0.1 -C public --temp --humid --fahrenheit --ths 0 --warning 80,20: --critical 95,10:
 OK - N180D East MAT(0): 77.70F, N180D East MAT(0): 42%

 (Check just sensor 0, 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.0 monitoring dewpoint temperature and dewpoint delta is supported. The
 AKCP 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_akcp2.pl -H 192.168.0.1 -C public --dewdelta --fahrenheit --ths 0,1 --warning 10: --critical 5:
 OK - N180D East MAT(0) Delta: 65.34F, N180B West MAT(1) Delta: 64.90F

 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 AKCP 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_akcp2.pl -H 192.168.0.1 -C public --temp --humid --dewdelta --fahrenheit --ths 0,1 \
   --warning 80,50,10: --critical 90,80,5: 
 OK - N180D East MAT(0): 77.50F, N180D East MAT(0): 43%, N180D East MAT(0) Delta: 65.69F, \
 N180B West MAT(1): 76.40F, N180B West MAT(1): 42%, N180B West MAT(1) Delta: 65.50F

 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 Other Sensors

 Unfortunately there is currently no support for other sensors, such as water, airflow, 4-20ma, etc.
 This script only supports temperature and humidity probes at this time. In the future support may
 be added for more sensors as demand for these features increase. My suggestion is to simply use
 check_snmp to monitor these types of metrics. 

=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 AKCP 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_akcp2.pl -H 192.168.0.1 -C public --temp --warning :77 --critical :78 --ths 0,1,2
 CRITICAL - N180D East MAT(0): 78.80F, , WARNING - N180B West MAT(1): 77.70F

 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) 2015 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.

