日別アーカイブ: 2014年4月19日

MacBookやLinuxノートパソコンのバッテリー残量をウォッチしてImKayacでiPhoneに通知を送るPerlプログラムを作ったら地味に便利だった

仕事と個人で合計MacBook Air 3台に囲まれている おがた (@xtetsuji) です。

最近は複数のMacBook Airに囲まれている生活をしているのですが、現状バッテリーは自宅も会社も一つしかコンセントに繋いでいないという状況でなんとかなっています。ひとえにMacBook Airのバッテリーが持つから。一方の充電が完了したら、もう一方に充電ケーブルをつなぎ替えるだけで良いんです。当然ながら製品購入時に付いてくるものや予備で買ったものも含めて、ACアダプタは複数持ってはいますが、電源を挿す口が近くに足りていなかったり、会社に予備を置くのが面倒とか、そんな背景があります。

とはいえズボラな性格なので、こっちのバッテリーに充電してそのまま放置していたら、あっちのバッテリー残量がピンチということも結構あります。そこでMacBook Airのバッテリー残量を定期的に監視して、必要に応じて手元のiPhoneにプッシュ通知してくれるプログラムが欲しいと思って、思うままにサッと書いてみました。それが思いのほか便利だったので、せっかくなのでブログでご紹介してみようと思って記事を書いてみた次第です。

必要なのはPerlです。できればシステムPerlではなくユーザPerlが良いでしょう。モジュールはコアモジュール以外ではAnyEvent、Cocoa::Growl (存在する場合) 、WebService::ImKayac::Simple に依存します。Cocoa::GrowlはMacにしか対応していないし、WebService::ImKayac::Simple は最近登場したモジュールなので、Debian/Ubuntu のパッケージにもなっていません。そういうことを考えるとやはりユーザPerlを作る必要がありますが、そのあたりはPerlbrewやplenvの記事に譲りたいと思います。

やっていること自体は単純なので、最近のPerlのコアのみでも、もしくはシェルスクリプトでも頑張れば書くことはできると思います。

デーモン化とかは全然考えていないプログラムで、”&” でバックグラウンドに回して使う系のコマンドです。個人ユースのプログラムは面倒なので無闇にデーモン化しないというのが個人的な趣味なだけです。デーモン化が好きな方はApp::Daemonなどを使って改造していただくか、nohup や disown などを使ってください。詳細はプログラム内のPODを見てみてください。

標準ではホームディレクトリに WebService::ImKayac::Simple の設定ファイルが “.imkayac.yml” という名前で存在する必要があります。当然ながらiPhoneでImKayacのアプリをダウンロードして登録している必要があります。設定ファイルの書式は WebService::ImKayac::Simple のドキュメントを参考にして下さい。

上記のようなお膳立てでバックグランドジョブとして起動すると、バッテリー残量を10分おきにウォッチして、20% 50% 80% を上回ったり下回ったりした場合にImKayacで通知を送信します。また現在のバージョンでは、Cocoa::GrowlがインストールされていればGrowlでの通知も行い、要らないかもしれませんがお節介にも標準出力にも出してくれます。また充電が100%になったときに満充電になったこともお知らせしてくれます。監視のインターバルやバッテリー残量のしきい値の数々は、コマンドライン引数で変更可能です。詳細はプログラム内ドキュメントを参照してください。

こんな感じで通知が来ます。便利。

battery-watchdの通知の様子

適切なGitHubのリポジトリがあれば入れようかと思ったんですが、どこに入れてよいかわからない書き捨てプログラムとなってしまったので、とりあえず現状のものを $VERSION = “0.01” としてGistに貼りました。

Linuxラップトップでも acpi コマンドでバッテリー残量を取得することが可能なので、それにも対応してみたつもりですが、現状Linuxラップトップが手元になかったので、この部分のコードはテストしていません。レポートお待ちしています。

まだ作りたてなので、色々と不具合のようなものがあるでしょう。レポートお待ちしています。

適切なリポジトリやパッケージ化の続報があれば、随時追記しています。要望ありましたら、Twitter @xtetsuji などにお気軽にお知らせください。

#!/usr/bin/env perl
# xtetsuji by 2014/04/19

our $VERSION = "0.01";

use strict;
use warnings;
use utf8;

use AnyEvent;
use Config;
#use Cocoa::Growl ':all';
use File::Basename qw(basename);
use Getopt::Long ();
use WebService::ImKayac::Simple;

use constant HAVE_COCOA_GROWL => eval {
    require Cocoa::Growl;
    import  Cocoa::Growl ':all';
    1;
};

if ( !HAVE_COCOA_GROWL ) {
    # Cocoa::Growl の無い環境ではとりあえず何もしないコマンドとして定義しておく
    *growl_register = sub {};
    *growl_notify   = sub {};
}

use constant APPLICATION_NAME => basename($0);
use constant GRAPH_DOWN       => -1;
use constant GRAPH_UP         =>  1;
use constant GRAPH_RELAX      =>  0;
use constant OSNAME           => $Config{osname};

my $p = Getopt::Long::Parser->new(
    config => [qw(posix_default no_ignore_case auto_help)]
);
$p->getoptions(
    'watch-percents=s'        => \my $watch_percents,
    'imkayac-config=s'        => \my $imkayac_config,
    'interval=i'              => \my $interval,
);

our $DEFAULT_INTERVAL = 600;

growl_register(
    app => APPLICATION_NAME,
    #icon => '',
    notifications => [qw/info/],
);

my $IMKAYAC_CONFIG_FILE = $imkayac_config || "$ENV{HOME}/.imkayac.yml";

if ( !-f $IMKAYAC_CONFIG_FILE ) {
    die qq(ImKayac config file "$IMKAYAC_CONFIG_FILE" is not found\n);
}

binmode STDOUT, ':utf8';

my @watch_percents = (20, 50, 80);

if ( $watch_percents ) {
    @watch_percents = split /,/, $watch_percents;
    if ( grep { !/^\d+$/ } @watch_percents ) {
        die "watch-percent option specify comma separated digits.\n";
    }
}

#chomp(my $hostname = `hostname`);
my $hostname = $Config{myhostname};

my $previous_percent = get_remaining(); # initialize

my $cv = AnyEvent->condvar;

my $im = WebService::ImKayac::Simple->new($IMKAYAC_CONFIG_FILE);

my $notify_callback = sub {
    my $response = shift;
    print $response . "\n"; # DEBUG?
    growl_notify(
        name => 'info',
        title => APPLICATION_NAME,
        description => $response,
    );
    $im->send(APPLICATION_NAME . ": " . $response . " ($hostname)"); # ok either flagged utf-8 or not.
};

my $timer = AnyEvent->timer(
    after    => 10,
    interval => $interval || $DEFAULT_INTERVAL,
    cb       => sub {
        my $current_percent = get_remaining();
        my $response = '';
        # process...
        for my $key (@watch_percents) {
            if ( my $res = graph_direction( $previous_percent => $current_percent, $key ) ) {
                if ( $res == GRAPH_UP ) {
                    $response = "${key}% を上回りました。現在${current_percent}%です。";
                }
                elsif ( $res == GRAPH_DOWN ) {
                    $response = "${key}% を下回りました。現在${current_percent}%です。";
                }
            }
        }
        if ( $previous_percent != 100 && $current_percent == 100 ) {
            $response = "満充電されました。";
        }

        if ( $response ) {
            $notify_callback->($response);
        }

        # reinitialize
        $previous_percent = $current_percent;
    },
);

$cv->recv();

sub get_remaining {
    if ( OSNAME eq 'darwin' ) {
        return get_remaining_mac()
    } elsif ( OSNAME eq 'linux' ) {
        return get_remaining_linux();
    } else {
        die "Unsupported your architecture yet\nPlease contact to \@xtetsuji by Twitter if you want to use this program!\n";
    }
}

sub get_remaining_mac {
    my $pmset = `pmset -g ps`;
    my ($percent) = $pmset =~ /(\d+)%; /;
    return $percent;
}

# 追加してみたけどまだ試していない
sub get_remaining_linux {
    my $acpi = `acpi -b`;
    my ($percent) = $acpi =~ /(\d+)%, /;
    return $percent;
}
# see: http://polamjag.hatenablog.jp/entry/2013/10/23/125843

sub graph_direction {
    my ($prev, $cur, $thr) = @_;
    if ( grep { !/^\d+$/ } ($prev, $cur, $thr)  ) {
        require Carp;
        Carp::croak "graph_direction error. ($prev, $cur, $thr)";
    }
    if ( $cur < $thr && $thr < $prev ) {
        return GRAPH_DOWN;
    }
    elsif ( $prev < $thr && $thr < $cur ) {
        return GRAPH_UP;
    }
    else {
        return GRAPH_RELAX;
    }
}

=pod

=head1 NAME

battery-watchd - battery watcher and observer for Mac and Linux laptop

=head1 SYNOPSIS

 battery-watchd &

=head1 OPTIONS

=head2 --watch-percents

 battery-watchd --watch-percents=5,10,15,20

Specify watch percents separated by comma.

=head2 --imkayac-config

 battery-watchd --imkayac-config=/path/to/config.yml

Specify your ImKayac config file path.

Default path is "$ENV{HOME}/.imkayac.yml".

This file format is YAML format. See below CONFIG FILE SYNTAX section.

=head2 --interval

 battery-watchd --interval=600

Specify watching interval seconds.

Default may be 600 seconds. You confirm it by following command.

 grep DEFAULT_INTERAVAL `which battery-watched`

=head1 CONFIG FILE SYNTAX

You can give a battery state by ImKayac.
So you have to tell this program ImKayac setting.
This program gives ImKayac setting file of YAML file.
It syntax is same as L<WebService::ImKayac::Simle>'s format.

Setting file's path is below "--imkayac-config" section.

=head1 DEPENDENCIES

L<AnyEvent>,
L<Cocoa::Growl>,
L<WebService::ImKayac::Simple>,
and some Perl5 core modules.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2014 by OGATA Tetsuji

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut