#! /usr/local/bin/perl5 -w # FreeBSD ups monitoring daemon # Box labeled "APC Battery Backup 500 VA 300 Watts" # Box labeled "APC Back-UPS ES Series" # Rumored to be same as the CyberFort 500 (I can't confirm this) # Shipping section says: # Supplier Part NO: BE500U # Supplier Part Description: BK ES USB 500VA 120v # LICENSE # BSD style. # # You can do what you want with it, commerical or otherwise, # so long as proper and faithful attribution is kept. # # This code has no warrantee - don't sue me. # FreeBSD uhid0 shows this device when I plug it in: # 23:56 PDT | jfesler@heaven:~/ % sudo usbdevs # addr 1: UHCI root hub, Intel # addr 2: Back-UPS ES 500 FW:2.e2.D USB FW:e2, APC use Sys::Syslog qw(:DEFAULT setlogsock); use strict "vars"; use vars qw(%syslog $line $battery $charge $oline $obattery $ocharge $lasttime $program ); my $uhid = "/dev/uhid0"; # This would be your USB device (run dmesg) my $wall = "/usr/bin/wall"; my $usewall = 1; my $shutdown_at = 30; # Shut down at 30 battery life my $shutdown = "/sbin/shutdown -h now power_failure"; my $useshutdown = 1; my $notify = 'root'; my $sendmail = '/usr/sbin/sendmail -t'; my $hostname = `hostname`; $program = $0; $program =~ s#.*/##; $syslog{"actions"} = 'user|alert'; $syslog{"change"} = 'user|alert'; $syslog{"debug"} = 'user|debug'; # Initialize some variables $line = $battery = $charge = $oline = $obattery = $ocharge = ""; $lasttime = time; setlogsock('unix'); openlog($program, 'cons,pid', 'user'); # Unbuffer output select(STDERR); $|=1; select(STDOUT); $|=1; # Take the USB device open(UHID,"<$uhid")||die "Failed to open $uhid : $!"; # Sync up with the data by monitoring the output # stream and looking at the clock. We are looking for # groups of 9 characters. do { print "synchronizing\n" } while (length(getblock_timebased()) % 9 ); # Read in each block of data, and set status accordigly. # Afterwords, check the status to see if any reactions are needed. while(1) { scan_data(); check_status(); } sub scan_data { my $buffer; read UHID,$buffer,9; die "Short read on UHID device" unless (length($buffer) == 9); my @buffer = split(//,$buffer); my @ord = map ( ord($_), @buffer); my @hex = map ( sprintf('%02x',$_), @ord); my $hexstring = join(" ",@hex); syslog($syslog{"debug"}, '%s', $hexstring); print "@hex\n"; my($command,$value) = (@ord); $hexstring =~ m/^06 08/ and do { $line = "on"; return; }; $hexstring =~ m/^06 10/ and do { $line = "off"; return; }; $hexstring =~ m/^16 0a/ and do { $battery = "discharging"; return; }; $hexstring =~ m/^16 0c/ and do { $battery = "full"; return; }; $hexstring =~ m/^16 0d/ and do { $battery = "charging"; return; }; $hexstring =~ m/^0c/ and do { $charge = $ord[1]; return; }; print "unexpected: $hexstring\n"; } sub check_status { # compare status if (($lasttime - time < 60) && ($battery eq $obattery) && ($charge eq $ocharge) && ($line eq $oline) ) { return; } $lasttime = time; # Generate the status string to report. Send it to syslog # and to stdout. my $message = "STAT battery=$battery line=$line charge=$charge \%"; print "$message\n"; syslog($syslog{"change"}, '%s', $message); # Are we on battery? if ($line eq "off") { handle_battery() if ($oline eq "on"); handle_percent() if (int($charge / 10) != int($ocharge/10)); handle_halt() if ($charge <= $shutdown_at); } # Are we back on the mainline? if ($line eq "on") { handle_mainline() if ($oline eq "off"); } $oline = $line; $obattery = $battery; $ocharge = $charge; } sub notify($$) { my($subject,$body) = @_; if ((defined $notify) && (length($notify))) { open(SENDMAIL,"|$sendmail"); print SENDMAIL <<"EOF"; To: $notify Subject: $subject $body EOF close SENDMAIL; } } sub wall($) { my($message) = @_; if ($usewall) { open(WALL,"|wall"); print WALL $message; close WALL; print "Sent to WALL: $message\n"; } else { $message =~ s/[\r\n]+/|/g; syslog($syslog{"actions"}, '%s', "Would have walled: " . substr($message,0,200)); } } sub handle_percent() { syslog($syslog{"actions"},'%s',"calling handle_percent()"); notify("UPS at $charge","host $hostname at $charge \% remaining"); my $message = <<"EOF"; We are now on battery power; $charge \% remaining. At $shutdown_at \% we will halt the computer. Save your work. You will be notified if the power is restored. EOF wall($message); } sub handle_halt() { if ($useshutdown) { handle_halt_yes(); } else { handle_halt_no(); } } sub handle_halt_no() { syslog($syslog{"actions"},'%s',"calling handle_halt_no()"); notify("UPS near death at $charge","host $hostname at $charge \% remaining"); my $message = <<"EOF"; The system would shut down now, but shutdowns are currently enabled. In all likelihood this host will lose power. Please save your work, type "sync". Log out. If you do not, there is a chance your data will be trashed. EOF wall($message); sleep 30; } sub handle_halt_yes() { syslog($syslog{"actions"},'%s',"calling handle_halt_yes()"); notify("UPS near death at $charge; halting","host $hostname at $charge \% remaining; halting"); my $message = <<"EOF"; We are now halting the system; we are only at $charge % battery remaining. EOF wall($message); $shutdown = "shutdown -h now" unless (defined $shutdown); system($shutdown) if ($useshutdown); sleep 3600; exit 1; # We hope to be dead by now. } sub handle_battery() { syslog($syslog{"actions"},'%s',"calling handle_battery()"); notify("UPS on battery power","host $hostname at $charge \% remaining"); my $message = <<"EOF"; The UPS has detected a power failure. We are now running on battery. Please save your work immediately. The battery indicator will count down from 100 \% to 20 \% full; at that time this host will be shut down if power has not yet been restored. Note: This is a cheap UPS. Don't expect this to last long. SAVE NOW. EOF wall($message); } sub handle_mainline() { syslog($syslog{"actions"},'%s',"calling handle_mainline()"); notify("UPS on main power","host $hostname at $charge \% remaining"); my $message = <<"EOF"; The UPS has detected A/C power has been restored. The shutdown has been canceled. EOF wall($message); } # Take as many bytes as you can from when you start to get data up to the # point where you have been idle for more than 2 seconds We will use this to # attempt to synchronize with the USB serialized stream data sub getblock_timebased { local $SIG{"ALRM"} = sub { die "timeout" ;}; alarm(10); my ($return,$buffer); eval { while(my $r = read UHID, $buffer,1) { alarm(2); $return .= $buffer; } }; alarm(0); return $return; } __END__ # Blocks are 9 bytes each # Sync using alarm() # Once synced, things seem OK # Each output is 9 bytes # byte 0 = code # byte 1 = val # remainder = garbage # code 0c = charge (0x00? - 0x64) # code 06 = line state # 06 08 = line # 06 10 = battery # code 16 = charge status # 16 0d = charge # 16 0c = full # 16 0a = discharge