#! /usr/bin/perl -w

# Waider 26/09/2000
# 30/09/2002 Modified to use /proc/apm.
# 11/08/2003 Add ACPI support
package Monitor::APM;

use Monitor;
@ISA = "Monitor";

sub init {
    my $mon = shift;

    # Do basic init
    $mon->SUPER::init();

    # stash as a bunch of references
    $mon->{'starttime'} = time;
    $mon->{'lasttime'} = time;
    $mon->{'timeleft'} = 0;
    $mon->{'start'} = -1;
    $mon->{'last'} = -1;
    $mon->{'online'} = 0;

    $mon;
}

sub newdata {
    my $mon = shift;
    my $text = $mon->{'text'};
    my $apm = "";
    my @tips;

    print STDERR "Updating...\n";

    # XXX could probably open and rewind repeatedly, rather than open/close
    if ( open( APM, "/proc/apm" )) {
        $apm = <APM>;
        close( APM );
    } elsif ( -d "/proc/acpi/battery" ) {
        $apm = acpi_to_apm( $mon );
        push @tips, "ACPI";
    } else {
        $text = "APM not\navailable";
    }
    chomp( $apm );

    # From the kernel 2.4 sources (arch/i386/kernel/apm.c):
    my ( $drvver, $biosver, $flags, $acstat, $btstat, $btflag, $btpercent, $bttime, $bttime_unit )
      = split( / /, $apm );

    $btpercent ||= "-1";
    $btflag ||= "0xff";
    $btstat ||= "0xff";
    $acstat ||= "0xff";

    $btpercent = "??" if $btpercent == -1;

    # GUH.
    $flags = eval( $flags );
    $acstat = eval( $acstat );
    $btstat = eval( $btstat );
    $btflag = eval( $btflag );

    if ( defined( $mon->{'DEBUG'}) && $mon->{'DEBUG'} ) {
        print "$apm\n";
        print "Driver version: $drvver BIOS version: $biosver\n";
        printf ( "Flags: %02x\n", $flags );
        printf ( "A/C Status: %02x\n", $acstat );
        printf ( "Battery Status: %02x\n", $btstat );
        printf ( "Battery Flags: %02x\n", $btflag );
    }

    $text = "$btpercent\n";

    # Useful things:
    # flags:
    #   0x1 16bit APM, 0x2 32bit APM, 0x4 IDLE slows clock, 0x8, BIOS disabled, 0x10 BIOS disengaged
    # acstat:
    #   0x0 offline, 0x1 online, 0x2 backup power, 0xff unknown
    # btstat:
    #   0x0 high, 0x1 low, 0x2 critical, 0x3 charging, 0x4 selected batt not present, 0xff unknown
    # btflag:
    #   0x1 high, 0x2 low, 0x4 critical, 0x8 charging, 0x40 no sys batt, 0xff unknown
    # btpercent/bttime: -1 => unknown
    # bttime_unit:
    #   min or sec

    # do this with a hash!
    if ( $flags & 0x1 ) {
        push @tips, "16-bit APM";
    }
    if ( $flags & 0x2 ) {
        push @tips, "32-bit APM";
    }
    if ( $flags & 0x4 ) {
        push @tips, "IDLE slows clock";
    }
    if ( $flags & 0x8 ) {
        push @tips, "BIOS diabled";
    }
    if ( $flags & 0x10 ) {
        push @tips, "BIOS disengaged";
    }

    # Grab state
    my $starttime = $mon->{'starttime'};
    my $lasttime =  $mon->{'lasttime'};
    my $timeleft =  $mon->{'timeleft'};
    my $start =     $mon->{'start'};
    my $last =      $mon->{'last'};
    my $online =    $mon->{'online'};

    # Are we plugged in or not?
    if ( $acstat & 0x01 ) {
        if ( !$online ) {
            $online = 1;
            $start = $btpercent;
            $last = -1;
        }
    } else {
        if ( $online ) {
            $start = $btpercent;
            $last = $btpercent;
            $starttime = time;
            $lasttime = time;
            $online = 0;
        }
    }

    my $warn = "";
    if ( $btstat == 0x02 or $btflag & 0x02 ) {
        $warn = " (LOW)";
    }
    $text = "$btpercent$warn\n";

    # dummy, to catch the first run through
    $start = $btpercent if ( $start == -1 );
    $last = $start if ( $last == - 1 );

    # see if battery percentage has decreased
    if ( $last > $btpercent ) {
        $timeleft = $btpercent * ( time - $starttime ) / ( $start - $btpercent );
        $last = $btpercent;
    }

    # now screw all that if the BIOS actually gave us usable time
    if ( $bttime != -1 ) {
        if ( $bttime_unit eq "min" ) {
            $bttime *= 60;
        }
        $timeleft = $bttime;
    }

    # and format that
    if ( !$online ) {
        $text .= sprintf( "%02d:%02d:%02d", (int( $timeleft / 3600 )),
                          int( $timeleft / 60 ) % 60, $timeleft % 60 );
    } else {
        if ( $btstat == 0xff && $btflag == 0xff ) {
            $text .= "mains(?)";
        } else {
            if ( $btstat == 0x03 or $btflag & 0x8 ) {
                $text .= "charging";
            } else {
                $text .= "mains";
            }
        }
    }

    # Store state
    $mon->{'starttime'} = $starttime;
    $mon->{'lasttime'}  = $lasttime;
    $mon->{'timeleft'}  = $timeleft;
    $mon->{'start'}     = $start;
    $mon->{'last'}      = $last;
    $mon->{'online'}    = $online;

    if ( @tips ) {
        $mon->set_tooltip( join( ", ", @tips  ));
    }

    $text;
}

sub acpi_to_apm {
    my $mon = shift;
    # Convert the info in /proc/acpi/battery/* into an APM string. Loses info, but screw that!
    my $apm;
    my ( $drvver, $biosver, $flags, $acstat, $btstat, $btflag, $btpercent, $bttime, $bttime_unit )
      = ( "1.4", "1.1", 0, 0, 0, 0, -1, -1, "?" );

    # here's the output for a battery:
    # present:                 yes
    # design capacity:         54719 mWh
    # last full capacity:      53913 mWh
    # battery technology:      rechargeable
    # design voltage:          14399 mV
    # design capacity warning: 5391 mWh
    # design capacity low:     3235 mWh
    # capacity granularity 1:  2 mWh
    # capacity granularity 2:  2 mWh
    # model number:            Primary
    # serial number:           1FA50011
    # battery type:            LIon
    # OEM info:                 COMPAQ
    # present:                 yes
    # capacity state:          ok
    # charging state:          unknown
    # present rate:            0 mW
    # remaining capacity:      52530 mWh
    # present voltage:         16875 mV
    #
    # when not on mains, charging state => discharging and present rate => rate of discharge

    # get the ac adapter state for acstat
    if ( opendir( DIR, "/proc/acpi/ac_adapter" )) {
        for my $dir ( grep !/^\.\.?$/, readdir( DIR )) {
            if ( open( ACPI, "/proc/acpi/ac_adapter/$dir/state" )) {
                my $state = <ACPI>;
                print STDERR "AC $dir $state" if $mon->{'DEBUG'};
                $acstat |= 0x1 if $state =~ /on-line/;
                close( ACPI );
            } else {
                print STDERR "Failed to get AC $dir state: $!\n" if $mon->{'DEBUG'};
            }
        }
    }

    opendir( DIR, "/proc/acpi/battery" );
    my @batteries = grep !/^\.\.?$/, readdir( DIR );
    closedir( DIR );

    my ( $max, $lev, $low, $crit, $rate ) = ( 0, 0, 0, 0, 0 );

    for my $battery ( @batteries ) {
        open( BATT, "/proc/acpi/battery/$battery/info" );
        my @bits = <BATT>;
        next unless $bits[0] =~ /yes/;

        close( BATT );

        open( BATT, "/proc/acpi/battery/$battery/state" );
        push @bits, <BATT>;
        close( BATT );

        if ( $mon->{'DEBUG'} || 0 ) {
            print STDERR "Battery $battery\n";
            print STDERR join( "", @bits );
            print STDERR "\n";
        }

        for my $bits ( @bits, @bits ) { # stupidity!
            chomp( $bits );
            my ( $field, $value ) = split( /:\s*/, $bits, 2 );
            $value =~ s/\s+$//;
            if ( $field eq "last full capacity" ) { #"design capacity" ) {
                ( $max ) = $value =~ /(\d+)/;
            } elsif ( $field eq "remaining capacity" ) {
                ( $lev ) = $value =~ /(\d+)/;
            } elsif ( $field eq "design capacity warning" ) {
                ( $low ) = $value =~ /(\d+)/;
            } elsif ( $field eq "design capacity low" ) {
                ( $crit ) = $value =~ /(\d+)/;
            } elsif ( $field eq "charging state" ) {
                if ( $value eq "unknown" ) {
                    $btstat = 0xff;
                    $btflag = 0xff;
                } elsif ( $value eq "discharging" ) {
                    if ( $lev ) {
                        if ( $lev > $low ) {
                            $btflag = 0x00;
                            $btstat |= 0x1;
                        } elsif ( $lev > $crit ) {
                            $btflag = 0x01;
                            $btstat |= 0x2;
                        } else {
                            $btflag = 0x02;
                            $btstat |= 0x4;
                        }
                    }
                } elsif ( $value eq "charging" ) {
                    $btstat = 0x03;
                    $btflag |= 0x8;
                    $acstat |= 0x1; # xxx check power_resource
                } elsif ( $value eq "charged" ) {
                    $btstat = 0x00;
                    $btflag |= 0x1;
                    $acstat |= 0x1;
                }
            } elsif ( $field eq "present rate" ) {
                ( $rate ) = $value =~ /(\d+)/;
            }
        }
        last;                   # XXX
    }

    $btpercent = sprintf( "%02d%%", $lev / $max * 100 ) if ( $max );

    if ( $rate ) {
        $bttime_unit = "min";
        $bttime = ( $lev - $crit ) / $rate * 60;
    }

    $apm = sprintf( "%s %s 0x%02x 0x%02x 0x%02x 0x%02x %s %d %s",  $drvver, $biosver, $flags, $acstat, $btstat, $btflag, $btpercent, $bttime, $bttime_unit );

    print STDERR "APM: $apm\n" if $mon->{'DEBUG'};

    $apm;
}

1;
