#!/usr/bin/perl
##################################################
# Simple nuts shutdown-check client
#
# (C) 2007 Adrian Ulrich
#
# Released under the terms of The "Artistic License 2.0".
# http://www.perlfoundation.org/legal/licenses/artistic-2_0.txt
#

use strict;
use IO::Socket;

use constant VERSION      => 20071225;  # Script version
use constant WAIT_TIMEOUT => 100;       # Check UPS-Status each 100 seconds
use constant CRIT_TIMEOUT => 15;        # Check UPS each 15 seconds on powerloss
use constant NAG_TIMEOUT  => 120;       # Send low-voltage warning each 2 minutes
use constant ROWFAILS     => 5;         # Alert admin if polling failed 5 times in a row
use constant TOTALS       => 14400;     # Printout Grand-Totals each 4 hours
use constant GRACETIME    => 90;        # WALL a warning 90 seconds before shutting down

my $conf = { notify          => 'nobody@example.com',
             sendmail        => '/usr/sbin/sendmail',
             charge_warn     => 60,
             charge_shutdown => 35,
             upsname         => 'apc',
             upsd_host       => '127.0.0.1',
             upsd_port       => 3493,
           };


my $gstate = { rfail => 0, last_totals => 0, last_nag => 0, ups_serial => '--no serial--',
               is_online => 1, hostname => `hostname`
             };
chomp($gstate->{hostname});

xlog("Startin up $0, version ".VERSION);
main($conf);




sub main {
	my($conf) = @_;
	
	for(;;) {
		my $NOW   = time();
		my $ups   = QueryUps(Port=>$conf->{upsd_port}, Host=>$conf->{upsd_host}, Name=>$conf->{upsname});
		my $SLEEP = CRIT_TIMEOUT;
		
		if($ups->{'ups.status'}) {
			$gstate->{rfail} = 0;
			my %ups_status   = map { $_ => 1 } split(/ /,$ups->{'ups.status'});
			my $bcharge      = $ups->{'battery.charge'};
			
			if($gstate->{last_totals} < $NOW-(TOTALS)) {
				$gstate->{last_totals} = $NOW;
				LogUpsInformation($ups);
			}
			
			if($ups_status{'CHRG'}) {
				# Log a warning if UPS is still charging
				xlog("Warning: UPS is charging ($ups->{'battery.charge'}%)");
			}
			
			if($ups_status{'OL'} && !$gstate->{is_online}) {
				# UPS thinks it's online but we think it isn't: Switch state
				$gstate->{is_online} = 1;
				xnotify("AC-Power enabled!");
			}
			elsif(!$ups_status{'OL'} && $gstate->{is_online}) {
				# UPS reports not-online but we think it is: switch state
				$gstate->{is_online} = 0;
				xnotify("AC-Power lost!");
			}
			
			if(!$ups_status{'OL'}) {
				# Critical situation:
				if($bcharge < 1) {
					xnotify("Hups? UPS returned no charge information ($bcharge)");
				}
				elsif($bcharge <= $conf->{charge_shutdown}) {
					xnotify("Starting system shutdown ($bcharge <= $conf->{charge_shutdown}, No AC-Power)");
					system("echo \"This system will shutdown in ".GRACETIME." seconds due to power failure. LOG OFF NOW!\" | wall");
					sleep(GRACETIME||1);
					xlog("Going down... bye!");
					system("/sbin/halt");
					die; die; die;
					# NOT REACHED
				}
				elsif($bcharge <= $conf->{charge_warn} && $gstate->{last_nag} < $NOW-(NAG_TIMEOUT)) {
					$gstate->{last_nag} = $NOW;
					xnotify("AC-Power lost and low battery charge. (Currently: $bcharge%)");
				}
			}
			else {
				$SLEEP = WAIT_TIMEOUT; # Everything fine, switch to long delays
			}
		}
		elsif($gstate->{rfail}++ == ROWFAILS) {
			$gstate->{rfail} = 0; # Re-Send message if it still fails
			xnotify("Failed to fetch information for ups $conf->{upsname}")
		}
		sleep($SLEEP);
	}
}

#################################################################
# Query UPS via TCP
sub QueryUps {
	my(%args) = @_;
	
	my $result = {};
	$SIG{ALRM} = sub { die "TIMEOUT\n"; };
	my $sock   = undef;
	
	alarm(5);
	eval {
		$sock = IO::Socket::INET->new(PeerHost=>$args{Host}, PeerPort=>$args{Port}, Proto=>'tcp', Timeout=>2);
		die "Could not connect ($!)\n" unless $sock;
		print $sock "LIST VAR $args{Name}\r\n";
		while(<$sock>) {
			my $l = $_; chomp($l);
			if($l =~ /^VAR $args{Name} (\S+) "([^"]*)"$/) {
				$result->{lc($1)} = $2;
			}
			elsif($l =~ /^BEGIN /) {
				# void
			}
			else {
				last;
			}
		}
		print $sock "LOGOUT\r\n";
	};
	alarm(0);
	$sock->close if $sock;
	
	if($@) { $result->{ERROR} = $@; chomp($result->{ERROR}); }
	return $result;
}

#################################################################
# Log grand-totals
sub LogUpsInformation {
	my($ups) = @_;
	$gstate->{ups_serial} = $ups->{'ups.serial'};
	$gstate->{ups_name}   = $ups->{'ups.model'};
	xlog("INFO: Model=>$ups->{'ups.model'}, Firmware=>$ups->{'ups.firmware'}, Status=>$ups->{'ups.status'}");
	xlog("INFO: BatteryData=>$ups->{'battery.date'}, Charge=>$ups->{'battery.charge'}");
	xlog("INFO: BatteryRuntime=>$ups->{'battery.runtime'}, InputVoltage=>$ups->{'input.voltage'} OutputVoltage=>$ups->{'output.voltage'}");
}


#################################################################
# Send a mail
sub xnotify {
	my($msg) = @_;
	xlog($msg);
	open(SM, "| $conf->{sendmail} -t");
	print SM "From: UPS-Monitor <>\r\n";
	print SM "To: <$conf->{notify}>\r\n";
	print SM "Subject: UPS-Event: $msg\r\n\r\n";
	print SM "Critical UPS-Event on $gstate->{hostname}:\r\n";
	print SM "  $msg\r\n";
	close(SM);
}

#################################################################
# Write to syslog
sub xlog {
	my($msg) = @_;
	system("logger", "-t", 'upswatch', '--', $gstate->{ups_serial}.": ".$msg);
}

