#!/usr/bin/env perl
# upower-monitor -- display battery status notifications via UPower
#
# (c) 2014 Mantas Mikulėnas <grawity@gmail.com>
# Released under the MIT License (dist/LICENSE.mit)
use Net::DBus qw(:typing);
use Net::DBus::Reactor;
use feature qw(state);
use constant {
	UP_DEVICE_IFACE		=> "org.freedesktop.UPower.Device",
	DBUS_PROPERTY_IFACE	=> "org.freedesktop.DBus.Properties",
	NOTIFY_URGENCY_LOW	=> 0,
	NOTIFY_URGENCY_NORMAL	=> 1,
	NOTIFY_URGENCY_HIGH	=> 2,
};

my @STATE = qw(unknown charging discharging fully_charged
               pending_charge pending_discharge);

my %STATE = map {$STATE[$_] => $_} 0..$#STATE;

sub UPower {
	Net::DBus->system
	->get_service("org.freedesktop.UPower")
	->get_object(shift // "/org/freedesktop/UPower")
}

sub Notifications {
	Net::DBus->session
	->get_service("org.freedesktop.Notifications")
	->get_object("/org/freedesktop/Notifications")
}

sub notify {
	state $id = 0;
	my ($summary, %opts) = @_;

	if ($summary) {
		$id = Notifications->Notify(
			$opts{app} // "upowermon",
			$id,
			$opts{icon} // undef,
			$summary,
			$opts{body},
			$opts{actions} // [],
			$opts{hints} // {},
			$opts{timeout} // 3_000);
	} else {
		Notifications->CloseNotification($id);
	}
}

sub timedelta {
	my ($dif) = @_;
	my ($s, $m, $h, $d);

	$dif -= $s = $dif % 60; $dif /= 60;
	$dif -= $m = $dif % 60; $dif /= 60;
	$dif -= $h = $dif % 24; $dif /= 24;
	$d = $dif + 0;

	if ($d > 0)	{ "${d}d ${h}h" }
	elsif ($h > 0)	{ "${h}h ${m}m" }
	elsif ($m > 0)	{ "${m} mins" }
	else		{ "${s} secs" }
}

my $devices = 0;
my $notified = 0;

# battery percentage to warn about
my $warning = 15;
my $critical = 5;

for my $dev_p (@{UPower->EnumerateDevices()}) {
	my $dev = UPower($dev_p);
	if ($dev->Get(UP_DEVICE_IFACE, "IsRechargeable")) {
		print "Watching $dev_p\n";
		$dev
		->as_interface(DBUS_PROPERTY_IFACE) #sigh
		->connect_to_signal("PropertiesChanged", sub {
			my ($interface, $changed, $invalidated) = @_;
			return unless $interface eq UP_DEVICE_IFACE;
			return unless (exists $changed->{State} ||
			               exists $changed->{Percentage} ||
			               exists $changed->{TimeToEmpty});

			my $charge = $changed->{Percentage} //
			             $dev->Get(UP_DEVICE_IFACE, "Percentage");
			my $state = $changed->{State} //
			            $dev->Get(UP_DEVICE_IFACE, "State");

			my $status = "at $charge%";
			my $time = 0;
			my $suspend = 0;

			if ($state == $STATE{discharging}) {
				$time = $changed->{TimeToEmpty} //
				        $dev->Get(UP_DEVICE_IFACE, "TimeToEmpty");
				if ($time) {
					$status .= " (".timedelta($time)." until empty)";
				}
			}
			elsif ($state == $STATE{charging}) {
				$time = $changed->{TimeToFull} //
				        $dev->Get(UP_DEVICE_IFACE, "TimeToFull");
				if ($time) {
					$status .= " (".timedelta($time)." until full)";
				}
			}

			if ($state == $STATE{discharging}) {
				if ($charge <= $critical) {
					$status = "$status. Suspending";
					$suspend = 1;
				}
				elsif ($charge <= $critical+2) {
					$status = "$status. Will suspend at $critical%";
				}
			}

			print "Battery $STATE[$state], $status\n";

			if ($state == $STATE{discharging}) {
				if ($charge <= $warning) {
					my $icon = $dev->Get(UP_DEVICE_IFACE, "IconName");
					notify("Battery low",
						body => "The battery is $status.",
						icon => $icon,
						hints => {
							transient => dbus_boolean(1),
							urgency => dbus_byte(NOTIFY_URGENCY_HIGH),
						},
						timeout => 10_000);
					$notified = 1;
				} else {
					$notified = 0;
				}
			}
			elsif ($state == $STATE{charging}) {
				if ($notified != 2) {
					my $icon = $dev->Get(UP_DEVICE_IFACE, "IconName");
					notify("Battery charging",
						body => "The battery is $status.",
						icon => $icon,
						hints => {
							transient => dbus_boolean(1),
						});
					$notified = 2;
				}
			}

			if ($suspend) {
				system("systemctl", "suspend");
			}
		});
		++$devices;
	}
}

if (!$devices) {
	warn "No battery devices found\n";
	exit 1;
}

Net::DBus::Reactor->main->run;
