# $Id: BatteryMonitor.pm,v 1.13 2005/04/02 17:15:38 jodrell Exp $
# This file is part of PerlPanel.
# 
# PerlPanel is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# PerlPanel 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. See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with PerlPanel; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Copyright: (C) 2003-2005 Eric Andreychek <eric@openthought.net>
# Rewritten by David Lazar <dlazar@google.com>.

package PerlPanel::Applet::BatteryMonitor;
use strict;

our $VERSION = 1.0;

sub new {
	my $class = shift;
	my $self = {};
	$self->{package} = $class;
	return bless($self, $class);
}

sub configure {
	my $self = shift;
	$self->{widget} = Gtk2::EventBox->new;

	$self->{label}= Gtk2::Label->new();
	$self->{widget}->add($self->{label});
	$self->{config} = PerlPanel::get_config('BatteryMonitor');
	PerlPanel::tips->set_tip($self->{widget}, 'Battery Monitor');
	$self->{monitor} = Monitor::create();
	$self->{widget}->show_all();
	$self->update();
	PerlPanel::add_timeout($self->{config}->{interval}, sub { $self->update() });
	return 1;
}

our %STATUSES = (
	0 => {
		'symbol' => '*',
		'message' => 'The system is running on battery power.',
	},
	1 => {
		'symbol' => '|',
		'message' => 'The system is running on AC power.',
	},
	2 => {
		'symbol' => '-',
		'message' => 'The system is running on backup power.',
	},
);
our $UNKNOWN_STATUS = {
	'symbol' => '!',
	'message' => 'Unknown status',
};
	

sub update {
	my $self = shift;

	$self->{monitor}->refresh();
	my $ac_status = $self->{monitor}->get_ac_status();
	my $charge = $self->{monitor}->get_charge_percent();

	my $status = $STATUSES{$ac_status};
	$status = $UNKNOWN_STATUS unless (defined $status);

	PerlPanel::tips->set_tip($self->{widget}, _($status->{message}));
	$self->{label}->set_text(sprintf(' %d%% %s ', int($charge + 0.5), $status->{symbol}));

	return 1;
}

sub widget {
	my $self = shift;
	return $self->{widget};
}

sub expand {
	return 0;
}

sub fill {
	return 0;
}

sub end {
	return 'end';
}

sub get_default_config {
	return { 'interval' => 1000 };
}


# Monitor is an abstract base class of the various battery monitor
# classes.
#
# External users should call Monitor::create() to get a Monitor instance
# (the exact subclass is determined at runtime).  On the monitor
# instance, the refresh() method should be called to read the battery
# data from the kernel.  After refresh() has been called,
# get_ac_status() and get_charge_percent() can be used to query the
# power supply state.
package Monitor;

our $AC_UNKNOWN = -1;
our $AC_OFFLINE = 0;
our $AC_ONLINE = 1;
our $AC_BACKUP = 2;

# Returns a Monitor object.  The exact type of the returned object is
# determined at run-time, based on the detected kernel functionality.
sub create {
	return SysfsAcpiMonitor->new() if SysfsAcpiMonitor::is_supported();
	return ProcfsAcpiMonitor->new() if ProcfsAcpiMonitor::is_supported();
	return PmuMonitor->new() if PmuMonitor::is_supported();
	return ApmMonitor->new() if ApmMonitor::is_supported();
	return Monitor->new();
}

sub new {
	my $class = shift;
	my $self = {};
	$self->{ac_status} = $AC_UNKNOWN;
	$self->{batteries} = [];
	return bless($self, $class);
}

# Returns the status of the AC adapter.  Possible values are:
# $AC_ONLINE, $AC_OFFLINE, $AC_BACKUP or $AC_UNKNOWN
sub get_ac_status {
	my $self = shift;
	return $self->{ac_status};
}

# Returns the remaining battery charge, as a percentage of the maximum
# charge.
sub get_charge_percent {
	my $self = shift;

	# It appears that different batteries report their current charge
	# either in terms of electrical charge (mAh) or in terms of energy
	# (mWh).  This code doesn't really care about the units (since it only
	# computes percentages), but it does assume that all batteries in the
	# system report their status in the same measurement unit.

	my ($total_cur_charge, $total_max_charge) = (0, 0);
	for my $battery (@{$self->{batteries}}) {
		$total_cur_charge += $battery->[0];
		$total_max_charge += $battery->[1];
	}
	return 0 if ($total_max_charge == 0);

	return $total_cur_charge * 100 / $total_max_charge;
}

# This method should be overridden by the subclasses, to read the
# battery data from the kernel, and to populate the internal fields:
#
#   self->{ac_status}:
#	one of $AC_ONLINE, $AC_OFFLINE, $AC_BACKUP or $AC_UNKNOWN
#
#   self->{batteries}:
#	a reference to a list of pairs (current_charge, max_charge)
sub refresh {
}

# Clears the internal state of the monitor.
sub _clear {
	my $self = shift;
	$self->{ac_status} = $AC_UNKNOWN;
	$self->{batteries} = [];
}

# Parses a file looking for regexps, and uses them to extract values
# matched by groups in the regexps.
#
# The routine takes as arguments the name of the file to parse, and a
# reference to an array of matchers.  Each matcher is a tuple that
# contains a reference to a variable (where the output value will be
# stored) and a regexp.  Each regexp should contain one group, which
# will be extracted and stored into the referenced value.
#
# Returns true on success.
sub _extract_values {
	my $file = shift;
	my $matchers = shift;

	open(FILE, "<", $file)  or return 0;
	while (<FILE>) {
		for my $matcher (@$matchers) {
			${$matcher->[0]} = $1 if (/$matcher->[1]/);
		}
	}
	close(FILE)  or return 0;

	return 1;
}

# Returns the full content of a file.  Removes the final newline, if it
# is present.
sub _read_string {
	my $file = shift;

	open(FILE, "<", $file)  or return undef;
	my $content = scalar <FILE>;
	chomp($content);
	close(FILE)  or return undef;

	return $content;
}


# SysfsAcpiMonitor uses the sysfs ACPI interface for getting information
# about the power supply from the kernel.
package SysfsAcpiMonitor;
use base 'Monitor';

our $ACPI_DIR = "/sys/class/power_supply";

sub is_supported {
	return 0 unless (-d $ACPI_DIR);
	return 1;
}

sub refresh {
	my $self = shift;
	$self->_clear();

	$self->{ac_status} = _get_ac_status();
	for my $battery (_list_batteries()) {
		push(@{$self->{batteries}}, _get_battery_charge($battery));
	}
}

sub _get_ac_status {
	my $ac_status = $Monitor::AC_UNKNOWN;

	for my $ac_adapter (_list_ac_adapters()) {
		next unless (-f "$ac_adapter/online");

		my $state = _read_int("$ac_adapter/online");

		return $Monitor::AC_ONLINE  if ($state eq "1");
		$ac_status = $Monitor::AC_OFFLINE  if ($state eq "0");
	}
	
	return $ac_status;
}

sub _list_ac_adapters {
	return grep(_is_ac_adapter($_), <$ACPI_DIR/*>);
}

sub _is_ac_adapter {
	my $dir = shift;
	return 0 unless (-f "$dir/type");
	return 0 unless (Monitor::_read_string("$dir/type") eq "Mains");
	return 1;
}

sub _list_batteries {
	return grep(_is_battery($_), <$ACPI_DIR/*>);
}

sub _is_battery {
	my $dir = shift;
	return 0 unless (-f "$dir/type");
	return 0 unless (Monitor::_read_string("$dir/type") eq "Battery");
	return 0 unless (-f "$dir/present");
	return 0 unless (_read_int("$dir/present"));
	return 1;
}

sub _get_battery_charge {
	my $dir = shift;

	if (-f "$dir/charge_now" && -f "$dir/charge_full") {
		return [ _read_int("$dir/charge_now"),
			 _read_int("$dir/charge_full") ];
	}

	if (-f "$dir/energy_now" && -f "$dir/energy_full") {
		return [ _read_int("$dir/energy_now"),
			 _read_int("$dir/energy_full") ];
	}

	return [0, 0];
}

sub _read_int {
	my $file = shift;
	return int($1) if (Monitor::_read_string($file) =~ /^(\d+)$/);
	return 0;
}


# ProcfsAcpiMonitor uses the procfs ACPI interface for getting
# information about the power supply from the kernel.
package ProcfsAcpiMonitor;
use base 'Monitor';

our $ACPI_AC_DIR = "/proc/acpi/ac_adapter";
our $ACPI_BATTERY_DIR = "/proc/acpi/battery";

sub is_supported {
	return 0 unless (-d "$ACPI_AC_DIR");
	return 1;
}

sub refresh {
	my $self = shift;
	$self->_clear();

	$self->{ac_status} = _get_ac_status();
	for my $battery (_list_batteries()) {
		push(@{$self->{batteries}}, _get_battery_charge($battery));
	}
}

sub _get_ac_status {
	my $ac_status = $Monitor::AC_UNKNOWN;

	for my $ac_adapter (_list_ac_adapters()) {
		next unless (-f "$ac_adapter/state");

		my $state = "unknown";
		Monitor::_extract_values("$ac_adapter/state", [
			  [ \$state, qr/^state\s*:\s*(\S+)\s*$/i ],
			])  or return $Monitor::AC_UNKNOWN;

		return $Monitor::AC_ONLINE  if (lc($state) eq "on-line");
		$ac_status = $Monitor::AC_OFFLINE  if (lc($state) eq "off-line");
	}
	
	return $ac_status;
}

sub _list_ac_adapters {
	return grep(_is_ac_adapter($_), <$ACPI_AC_DIR/*>);
}

sub _is_ac_adapter {
	my $dir = shift;
	return 0 unless (-f "$dir/state");
	return 1;
}

sub _list_batteries {
	return grep(_is_battery($_), <$ACPI_BATTERY_DIR/*>);
}

sub _is_battery {
	my $dir = shift;
	return 0 unless (-f "$dir/info");
	return 0 unless (-f "$dir/state");
	return 1;
}

sub _get_battery_charge {
	my $dir = shift;
	my ($present, $cur_charge, $max_charge) = (0, 0, 0);

	Monitor::_extract_values("$dir/state", [
		  [ \$present,    qr/^present\s*:\s*(\S+)\s*$/i ],
		  [ \$cur_charge, qr/^remaining capacity\s*:\s*(\d+)\D*$/i ],
		])  or return [0, 0];

	return [0, 0] unless (lc($present) eq "yes");

	Monitor::_extract_values("$dir/info", [
		  [ \$max_charge, qr/^last full capacity\s*:\s*(\d+)\D*$/i ],
		])  or return [0, 0];

	return [ int($cur_charge), int($max_charge) ];
}


# PmuMonitor uses the /proc/pmu interface for getting information about
# the power supply from the kernel.
package PmuMonitor;
use base 'Monitor';

our $PMU_INFO_FILE = "/proc/pmu/info";
our $PMU_BATTERY_FILE = "/proc/pmu/battery_%d";

sub is_supported {
	return 0 unless (-f $PMU_INFO_FILE);
	return 1;
}

sub refresh {
	my $self = shift;
	$self->_clear();

	my $num_batteries = 0;
	Monitor::_extract_values($PMU_INFO_FILE, [
		  [ \$self->{ac_status}, qr/^AC Power\s*:\s*(\d+)\s*$/i ],
		  [ \$num_batteries,     qr/^Battery count\s*:\s*(\d+)\s*$/i ],
		])  or return;

	for my $battery_index (0 .. $num_batteries-1) {
		push(@{$self->{batteries}}, _get_battery_charge($battery_index));
	}
}

sub _get_battery_charge {
	my $battery_index = shift;

	my ($cur_charge, $max_charge) = (0, 0);
	Monitor::_extract_values(sprintf($PMU_BATTERY_FILE, $battery_index), [
		  [ \$cur_charge, qr/^charge\s*:\s*(\d+)\s*$/i ],
		  [ \$max_charge, qr/^max_charge\s*:\s*(\d+)\s*$/i ],
		])  or return [0, 0];

	return [ int($cur_charge), int($max_charge) ];
}


# ApmMonitor uses the /proc/apm interface for getting information about
# the power supply from the kernel.
package ApmMonitor;
use base 'Monitor';

our $APM_FILE = "/proc/apm";
our $APM_RE = qr/^\S+ \d+\.\d+ 0x[0-9A-Fa-f]{2} 0x([0-9A-Fa-f]{2}) 0x([0-9A-Fa-f]{2}) 0x[0-9A-Fa-f]{2} (\d+)% -?\d+ \S+$/;

sub is_supported {
	return 0 unless (-f $APM_FILE);
	return 1;
}

sub refresh {
	my $self = shift;
	$self->_clear();

	if (Monitor::_read_string($APM_FILE) =~ /$APM_RE/) {
		# AC line status can be one of the following:
		#    0x00: Off-line
		#    0x01: On-line
		#    0x02: On backup power (BIOS >= 1.1 only)
		#    0xff: Unknown
		$self->{ac_status} = hex($1);

		# Battery status can be one of the following values:
		#    0x00: High
		#    0x01: Low
		#    0x02: Critical
		#    0x03: Charging
		#    0x04: Selected battery not present (BIOS >= 1.2 only)
		if (hex($2) < 4) {
			push(@{$self->{batteries}}, [ int($3), 100 ]);
		}
	}
}

1;
__END__
