#!/usr/local/bin/perl
#
# $Header: /home/vikas/src/nocol/perlnocol/RCS/apcmon,v 1.2 1998/07/31 18:41:34 vikas Exp $
#
# Monitor an APC SmartUPS. Part of the NOCOL network monitoring package.
#
# Maintained and updates by Vikas Aggarwal, vikas@navya.com
# Original submission by  cr@jcmax.com  (Cyrus Rahman), Dec 1996 
#
# Logic extracted from the "apcd"  daemon.
#
# If a file named 'apc_shutdown' exists in confg file directory, will ask the
# UPS to suspend itself after the grace period.
#
#####################
# 		The APC serial protocol
# Cable:
# 	Require a simple 3 wire cable connected as follows:
#
#		PC (9 pin)	APC
#		 2 RxD		 2
#		 3 TxD		 1
#		 5 GND		 9
#
# The APC SmartUPS uses a simple ASCII protocol at 2400 baud, 8N1. Your
# computer sends a character to the UPS and it answers back with an
# ascii string terminated with a CR/LF.
# A protocol description is given in the file apc-info
##
#
############################
## Variables customization #  overrides values in the nocollib.pl library
############################
#$varunits="Usage" ;			# the var.units field in EVENT struct
$sleepint=60;       			# Seconds to sleep between tries.
$sender = "apcmon";
############################
$debug = 0;				# set to 1 for debugging output
$libdebug = 0;				# set to 1 for debugging output
$prognm = $0;				# save program name

require "nocollib.pl" ;

# APC Smart UPS commands, by Jorg Wunsch, Dresden and Ty Hoeffer
#  <joerg_wunsch@interface-business.de> & <pth3k@galen.med.virginia.edu>
# This is an associative array of the command strings. Each of these maps
# into a configuration file keyword.
#
%UPSCMDS =
    (
     'UPS_CMD_INCR',    '+',
     'UPS_CMD_DECR',    '-',
     'UPS_CMD_MODEL',   ('a' & 0x1F),
     'UPS_CMD_TURNON',  ('n' & 0x1F),
     'UPS_CMD_XXX1',    ('z' & 0x1F),
     'UPS_CMD_PANEL',   'A',
     'UPS_CMD_BATTERY', 'B',
     'UPS_CMD_TEMP',    'C',
     'UPS_CMD_XXX3',    'D',
     'UPS_CMD_AUTOTEST', 'E',
     'UPS_CMD_FREQ',    'F',
     'UPS_CMD_XXX4',    'G',
     'UPS_CMD_SHUT_GRACE',  'K',
     'UPS_CMD_LINE',    'L',
     'UPS_CMD_MINLINE', 'M',
     'UPS_CMD_MAXLINE', 'N',
     'UPS_CMD_OUTPUT',  'O',
     'UPS_CMD_POWER',   'P',
     'UPS_CMD_STATUS',  'Q',
     'UPS_STATUS_STBY',  4,
     'UPS_STATUS_ON',    8,
     'UPS_STATUS_PWRF',  0x10,
     'UPS_STATUS_BLOW',  0x40,
     'UPS_CMD_DUMB',    'R',
     'UPS_CMD_SELFT1',  'U',
     'UPS_CMD_VERS',    'V',
     'UPS_CMD_SELFT2',  'W',
     'UPS_CMD_XXX5',    'X',
     'UPS_CMD_SMART',   'Y',
     'UPS_CMD_SHUT_IMM', 'Z',
     'UPS_CMD_SHOW_CMDS', 'a',
     'UPS_CMD_FIRMWARE',  'b',
     'UPS_CMD_LOCALID',   'c',
     'UPS_CMD_RETTHRESH', 'e',
     'UPS_CMD_FILL',      'f',
     'UPS_CMD_NBATTERY',  'g',
     'UPS_CMD_XXX6',      'j',
     'UPS_CMD_ALDELAY',   'k',
     'UPS_CMD_LOWXFR',    'l',
     'UPS_CMD_MDATE',     'm',
     'UPS_CMD_ID',        'n',
     'UPS_CMD_NLINE',     'o',
     'UPS_CMD_SHUTDELAY', 'p',
     'UPS_CMD_LOWB_WARN', 'q',
     'UPS_CMD_SHUTRETURN', 'r',
     'UPS_CMD_SENSITIVITY', 's',
     'UPS_CMD_UPXFR',      'u',
     'UPS_CMD_REPLDATE',   'x',
     'UPS_CMD_COPYRIGHT',  'y',
     'UPS_CMD_XXX7',       'z',
     'UPS_RES_OK',         "OK",
     'UPS_RES_NA',         "NA",
     'UPS_RES_SM',         "SM",
     'UPS_RES_BYE',        "BYE",
     'UPS_RES_NO',         "NO",
     'UPS_RES_PROG',       "PROG",
     'UPS_EV_POWER',       '!',
     'UPS_EV_PWRBACK',     '\$',
     'UPS_EV_BATLOW',      '%',
     'UPS_EV_BATUP',       '+',
     'UPS_EV_TURNON',      '?'
   );

##
# Read the config file. Use '\t' as a separator for 'item' list
sub readconf {
    open(CONFIG,"<$cfile")||die("Couldn't find $cfile, exiting");
    while(<CONFIG>)
    {
	chop;
	if(/^\s*#/) {next;}   # skip comments
	if(/^\s*$/) {next;}   # skip blank lines
	   # variable  normal-value  units  wLevel eLevel cLevel
	if (/\s*(\S+)\s+(\d+|-)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s*/)
	{
	    if (!$ups_name) {
		print "UPS_NAME not defined at line: $_\n";
		exit 1;
	    }
	    $item="$ups_name\t$1" ;	 # the item to monitor
	    $nominal{$item} = $2; # Nominal value
	    $wlevel{$item} = $4; # Warning delta
	    $elevel{$item} = $5; # Error delta
	    $clevel{$item} = $6; # Critical delta
	    push(@items,$item);

	    local($host, $addr, $junk) = split( /\t/, $item );
	    $varname = $1;
	    $varunits = $3;
	    &init_event ($host, "UnInit", $item); # fill in initial values
	}
	elsif (/PORT\s*(\S+)/i)
	{
	    $port = $1;
	}
	elsif (/UPS\S?NAME\s*(\S+)/i)   # UPS_NAME or UPSNAME
	{
	    $ups_name = $1;
	}
	else {print "Ignoring illegal line: $_\n";}

    }	# end while(CONFIG)

    # a generic variable indicating if the data could be polled or not.
    $item = "$ups_name\tUPSdata";
    push(@items,$item);
    $varname = "UPSdata";
    $varunits = "Avail";
    &init_event($ups_name, undef, $item);

    close(CONFIG);
    if(0>$#items){die("Nothing to monitor in $cfile, exiting")};
    if ($debug) {
	print "Items are:\n"; foreach (@items) { print "\t$_\n" } ;
    }
}				# end: readconf


#
## Check state of each UPS
##
#
sub dotest {
    local ($host, $ups) = @_ ;
    local ($isok) = (0);
    local ($Querycmd, $Query, $value, $nbytes);

    # See if we should shut the APC UPS down.
    # We only do the power down sequence if we can unlink the
    # shutdown file (to prevent looping shutdowns).
    if (-e "$etcdir/apc_shutdown") {
	if (unlink("$etcdir/apc_shutdown") == 1) {
	    # Turns off UPS.  Use a 'S' to suspend it instead (so that it
	    # will come back up when the power is restored).
	    if ($debug) {print "*****Suspending UPS*****\n"};
	    sleep 1;
	    syswrite(PORT, $UPSCMDS{'UPS_CMD_SHUT_GRACE'}, 1); sleep 1;
	    syswrite(PORT, $UPSCMDS{'UPS_CMD_SHUT_GRACE'}, 1); sleep 1;
	}
    }

    # Measure the variable
    $variable = (split("\t", $item))[1];

    if ($debug) {print "(debug): Checking $variable ($item)\n"};

    if ($variable eq "UPSdata") {
	$maxseverity = $E_CRITICAL;
	local ($retval) = $OkData;
	$OkData = 0;
	return ($retval, $retval);
    }

# Several indicators are derived from the status bytes:
#
# Status: 0 (0x08) - On, line power;
#	  1 (0x10) - On, no line power;
#	  2 (0x40) - On, no line power & battery low;
#	  3 (0x00) - off (standby), line power available & charging
#	  4 (0x04) - off (standby), no line power
#
# Q			Status byte 8 bits long.
#                               - 08  On-line LED ON
#				- 10  On Battery LED ON
#	                        - 18  Both LEDs ON
#                               - 02  UPS in shut down state, waiting for AC
#
#                              0 - If 1, UPS is in calibration
#                              1 - If 1, UPS is sleeping
#                              2 -  
#                              3 - If 1, AC is here
#                              4 - If 1, UPS is running on battery
#                              5 -
#                              6 - If 1, Battery is low
#                              7 - 

    # Query string
    if ($variable =~ /OnLine|AcPresent|OnBattery|BatteryLow/i)
    {
	$Querycmd = "UPS_CMD_STATUS";
    }
    else {
	$Querycmd = "UPS_CMD_" . $variable;
	$Querycmd =~ tr/a-z/A-Z/;    # make uppercase
    }

    if ($debug) {print "Query is $Querycmd\n"};
    $Query = $UPSCMDS{$Querycmd};     # extract string from assciative array
    if ($debug) {print "Sending $Query\n"};
    syswrite(PORT, $Query, 1);

    $value = 0;
    alarm(5);
    $nbytes = sysread(PORT, $value, 256);
    alarm(0);
    if ($nbytes) {
	$OkData = 1;
	chop $value;
	if ($debug) {print "Got value $value\n"};
	$value =~ s/^[!$%+?]*//g;
    }
    else {
	$maxseverity = $E_ERROR;
	if ($debug) {print "Got no returned data\n"};

	# Make sure UPS is in smart mode.  Obviously it won't be the first
	# time the read fails, but failing to init the first variable
	# one time shouldn't cause any trouble.  This way we check each
	# time, so if a new ups is brought up or a cable is connected
	# after the daemon starts all will work out.
	syswrite(PORT, "Y", 1);
	alarm(5);
	$nbytes = sysread(PORT, $value, 256);
	alarm(0);

	return (0, 0);
    }

    # extract the status from the boolean values
    SWITCH: {
	$variable =~ /OnLine/i && do {
	    $value = hex($value);
	    $value &= 0x02;
	    $value = ($value == 0) ? 1 : 0;
	    last SWITCH;
	};
	$variable =~ /AcPresent/i && do {
	    $value = hex($value);
	    $value &= 0x08;
	    $value = ($value != 0) ? 1 : 0;
	    last SWITCH;
	};
	$variable =~ /OnBattery/i && do {
	    $value = hex($value);
	    $value &= 0x10;
	    $value = ($value != 0) ? 1 : 0;
	    last SWITCH;
	};
	$variable =~ /BatteryLow/i && do {
	    $value = hex($value);
	    $value &= 0x40;
	    $value = ($value != 0) ? 1 : 0;
	    last SWITCH;
	};
    }

    # Calclulate delta
    if ($nominal{$item} ne '-') {$delta = &absolute($value - $nominal{$item}); }
    else {$delta = $value; }   # test real value

    ($isok, $varthres{$item}, $maxseverity) = 
	&calc_status ($delta, $wlevel{$item}, $elevel{$item}, $clevel{$item});

    if ($debug) {print "(debug): Status $isok, MaxSev= $maxseverity\n";}

    $siteaddr{$item} = "-";

    return ($isok, $value);

}	# end &dotest()

sub noresponse {
    if ($debug) {print "No Response alarm\n"};
    $OkData = 0;
}

sub absolute {
    local ($val) = @_;
    if ($val < 0) {$val *= -1;}
    return($val);
}

###
### main
###

## Since our dotest() and readconf() is pretty standard, we can call
## the nocollib.pl routine nocol_main() to do all the work for us...
{
    &nocol_startup;
    &readconf;
    if (!defined($port)) {
	die("PORT not defined in apcmon confg\n");
    }
    
    -c $port || die("Could not find UPS serial port $port, exiting");
    require "fcntl.ph";
#    sysopen(PORT, $port, &O_RDWR|&O_EXCL, 0) || die("Cannot open UPS port");
    open(PORT, ">+ $port") || die("Cannot open UPS serial port $port: $!\n");

    system("stty 2400 -opost -xcase -isig icanon -echo -istrip  icrnl igncr -ixon -parenb cs8 clocal < $port > $port");

    # Warn of unresponsive UPS.
    $SIG{'ALRM'} = "noresponse";

    while (1)
    {
	local ($stime, $deltatime);

	$stime = time;		# time starting tests

	foreach $item (@items) {
	    local ($host, $variable, $junk) = split( /\t/, $item );
	    local ($status, $value) = &dotest($host, undef);

	    if ($status < 0)	# bad status, probably some error
	    {
		if ($libdebug) { 
		   print STDERR "$me: dotest failed for $item... skipping\n";
		}
		next;
	    }
	    else {&update_event($item, $status, $value, $maxseverity);}
	}
	open(OEVENTS,">$datafile");
	foreach $item (@items)
	{
	    if(!$forget{$item})
	    {
		&writeevent(OEVENTS, $item);
	    }
	}
	close(OEVENTS);

	$deltatime = time - $stime;		# time to do tests

	if ($sleepint > $deltatime) { sleep($sleepint - $deltatime); }

    }				# end: while(1)
    
}	# end main()
