Amazon Dash ButtonとIRKitとLINE Message APIでバルスダッシュボタンを作る。

プライムデーでダッシュボタンが1個100円だったので、IRKitとLINEBotと連携して部屋の家電の電源を全部消すバルスダッシュボタンを作ることにしました。

使用したもの

  • Raspberry Pi 3
  • 天然水ダッシュボタン

今回はLINE botでCallbackを使用しないため、ローカルホストに適当にhttpで通信できるWebサーバが立っていれば十分です。

準備

ボタンのセットアップと、ボタン押下を検出してくれるdasherのインストールは以下の記事の内容をそのまま実施しました。ありがたいです。
Amazon Dash ButtonをただのIoTボタンとして使う – Qiita

ダッシュボタンが押された時に動作するWebhookの立ち上げ

(私が最速で作れる方法なので)hypnotoadとMojolicious::Liteでセットアップしました。
ローカルホストのポート8093で立ち上げてダッシュボタンの押下を待ち受けます。
動作としては、IRKitに家電の消灯信号を送った後、LINEの特定グループトーク宛にdasherからPOSTされたJSONの内容を送信します。

ソースはこんな感じ。

#!/usr/bin/perl
use utf8;
use strict;
use warnings;

use LINE::Bot::API;
use LINE::Bot::API::Builder::SendMessage;
use LWP::UserAgent;
use Mojolicious::Lite;

### CONSTS
my $ua   = new LWP::UserAgent();
my $log = app->log;
my $CHANNEL_ACCCESS_TOKEN = "xxx"; # LINE developersから取得したtokenを指定
my $CHANNEL_SECRET="xxx";

# API認証情報
my $bot = LINE::Bot::API->new(
    channel_secret       => $CHANNEL_SECRET,
    channel_access_token => $CHANNEL_ACCCESS_TOKEN,
    );

app->config(
    hypnotoad => {
      listen => ['http://*:8093'],
      workers => 2,
    },
);

sub call_api(){
  my $url = shift;
  my $method = shift;
  my $headers = shift;
  my $content = shift;

  my $req = HTTP::Request->new($method, $url);
  while (my ($key, $value) = each(%$headers)){
    $req->header($key => $value);
  }
  $req->content($content);
  return $ua->request($req);
}

my $IRKIT = "http://192.168.xx.xxx/messages"; # IRKitのエンドポイント
my %IRKIT_H = ("X-Requested-With" => "curl");
sub call_irkit(){
  my $content = shift;
  return &call_api($IRKIT, "POST", \%IRKIT_H, $content);
}

post '/barusu' => sub {
  my $self = shift;
  my $messages = LINE::Bot::API::Builder::SendMessage->new;
  my $data = $self->req->json; # dasherから送られたJSON

  my $light_off = '{"format":"raw","freq":38,"data":[消灯用データ]}';
  my $aircon_off = '{"format":"raw","freq":38,"data":[消灯用データ]}';
  # 他の消したい家電
  &call_irkit($light_off);
  &call_irkit($aircon_off);
  # 以下略
  $messages->add_text( text => $data->{text} );
  $bot->push_message("Cxx投稿先のグループID", $messages->build);
  return $self->render(json => {'status' => "OK. BARUSU."});
} => 'barusu';

app->start;

投稿先のグループIDだけはwebhook-event-object以外の確認方法がわからなかったので、適当にhttpsで通信できるサーバを立てて取得しました。
自分だけに飛ばすなら、自分のUserIdをLINE developersから確認できるのでここまでめんどくさくないと思います。

dasherの設定をする

dasherの設定(config/config.json)は以下のような感じです。

{"buttons":[
  {
    "name" : "天然水バルスダッシュボタン",
    "address": "ダッシュボタンのMACアドレス",
    "url": "http://127.0.0.1:8093/barusu",
    "method": "POST",
    "json": true,
    "body": {"text":"バルス!"}
  }
]}

使ってみる

dasherのREADMEどおり起動させました。

コマンドラインはこんな感じ

ボタンを押したところ電気が消えてエアコンも消えてテレビも消えました。(画像クリックでGIFアニメが再生されます。)

ポチッとな。

LINEには通知が飛びました。

LINEのようす。

これ、サーバで処理できることはなんでもボタン一つでできますね。とりあえずバルスボタンは玄関に設置しました。あと、作ってからLINEはあんまり関係なかったなと思いました。
もう1個ポテチのダッシュボタンがあるので何か別のものを作りたいですね。夢が広がります。

Twitterで強制的にPNG32でアップロードするツールを作った。

Twitterに画像をPNGでアップロードするツール
作りました。左上1pxの透明度を1%だけ落として強制的にPNG32でアップロードします。

Lenna(元画像)

Lenna(アップロード後)

差分

RGBAだと1だけ下がってるように見えます。ソースは以下。
retrorocket/twimage: Twitterで画像を強制的にPNGでアップできるようにする
PerlMagick便利ですね。

画像の引用元は以下です。
Lenna – Wikipedia

TwitterのアイコンをAPI経由でアップロードするツールに強制PNGモードを追加した。

TwitterのアイコンをAPI経由でアップロードするツール

ImageMagickとPerlMagickを入れるのがひたすらめんどくさいという理由だけで対応してなかったのですが、アイコンも丸くなるし強制JPGにされるしでうんざりしたので対応しました。
githubには後でpushしておきます。ソースはこんな感じで、左上の1pxの透明度を1から0.99に変更して元画像に極力影響ないようにPNG32に変換しています。

	#PNG変換モード
	if($self->session("png32")){
		my $img = Image::Magick->new;
		$img->Read($image_file);
		$img->Set(alpha => 'On');
		my @pixels = $img->GetPixel(x=>0,y=>0);
		$pixels[3]=0.998; #透明度を99%にする。
		$img->SetPixel(x=>0, y=>0, color=>\@pixels);
		binmode(STDOUT);
		$img->Write("PNG32:".$image_file);
		undef $img;
	}

やっぱりImageMagick便利ですね。クライアント側で変換させてもよかったのですが、upico使用ユーザ層のITリテラシー的にクライアントで変換させると余計トラブルになりそうなのでやめました。
みんなもうちょっと自分で調べるとかしてくれるといいんですが。

入れたImageMagickは7.0.6で、PerlMagickがうまいこと入らなかったので以下の記事を参考にしました。
ImageMagickインストール済みのとこからImage::Magick(PerlMagick)をコンパイル・インストールする方法 – Qiita
make perl-sources してなくてハマりました。

追記(2017/06/29)

座標の指定方法が間違ってたので修正しました。

LINE::Bot::APIとIRKitで部屋の家電を制御するサンプルコード。

解説書いてる時間がなかったのでサンプルコードだけ貼っておきます。そのうち書く。

LINEのmessage apiを使おうと思ってuse LWP::UserAgentとuse JSONでゴリゴリ書いてたら普通にPerlのSDKがあったときの顔をしていました。SDK充実しててすごい。
line/line-bot-sdk-perl: LINE: :API – SDK of the LINE Messaging API for Perl

私はWebhookのcallback先を自宅サーバ(Raspberry Pi 2)に設置してるのでIRKit Device HTTP APIを使用できます。というかInternet HTTP API使うのめんどくさいから自宅サーバをcallback先にしたというのもあります。
IBM BluemixとかHerokuとか使ってる人はInternet HTTP APIを使ってください。
アプリはhypnotoadとMojolicious::Liteで動かしていて、リバースプロキシに(無駄に)h2oを使ってるのですが、あまり意味は無いですね。

#!/usr/bin/perl

# 2017/01/26 ちょっと書き直した

use utf8;
use strict;
use warnings;

use LINE::Bot::API;
use LINE::Bot::API::Builder::SendMessage;
use LINE::Bot::API::Builder::TemplateMessage;
use LWP::UserAgent;
use Mojolicious::Lite;

my $ua = new LWP::UserAgent();

my $CHANNEL_ACCCESS_TOKEN = "xxx"; 
my $CHANNEL_SECRET="xxx";

# API認証情報
my $bot = LINE::Bot::API->new(
    channel_secret       => $CHANNEL_SECRET,
    channel_access_token => $CHANNEL_ACCCESS_TOKEN,
    );

app->config(
    hypnotoad => {
    listen => ['http://*:8092'],
    },
     );

# 当初いろんなリクエストを送る予定だった
sub call_api(){
  my $url = shift;
  my $method = shift;
  my $headers = shift;
  my $content = shift;

  my $req = HTTP::Request->new($method, $url);
  while (my ($key, $value) = each(%$headers)){
    $req->header($key => $value);
  }
  $req->content($content);
  return $ua->request($req);
}

# IRKitのAPIを呼び出す
sub call_irkit(){
  my $content = shift;
  my $IRKIT = "http://192.168.10.150/messages";
  my %IRKIT_H = ("X-Requested-With" => "curl");
  return &call_api($IRKIT, "POST", \%IRKIT_H, $content);
}

get '/' => sub {
  my $self = shift;
  return $self->render(template => 'index', format => 'html');
} => 'index'; #別になくても良い

post '/callback' => sub {
  my $self = shift;

  # LINEから受信したリクエストボディ
  my $source = $self->req->body;
  # シグネチャ検証
  unless ($bot->validate_signature($source, $self->req->headers->header('X-Line-Signature')) ) {
    return $self->render(json => {'status' => "failed to validate signature"});
  }

  my $events = $bot->parse_events_from_json($source);
  my $event = ${ $events }[0]; #多分先頭のイベントしか使わないので
  unless($event->is_message_event && $event->is_text_message){
    return $self->render(json => {'status' => "not text event"});
  }

  # 自分以外の人が操作できないようにする
  unless($event->user_id eq "my LINE ID"){
    return $self->render(json => {'status' => "You are not me."});
  }

  my $reply_text = $event->text;
  if($reply_text eq "コマンド教えて"){ # carouselの動作確認も兼ねてヘルプ

    my $carousel = LINE::Bot::API::Builder::TemplateMessage->new_carousel(
      alt_text => 'コマンドを教えます',
    );

    my @commands = ("電気つけて", "電気消して", "エアコンつけて", "エアコン消して");
    for my $i (@commands) { #carouselは5つまで
      my $column = LINE::Bot::API::Builder::TemplateMessage::Column->new(
        title     => "commands",
        text      => "利用頻度の高いコマンドです",
      )->add_message_action(
        label => $i,
        text  => $i,
      );
      $carousel->add_column($column->build);
    }
    my $messages = LINE::Bot::API::Builder::SendMessage->new()->add_template($carousel->build);
    $bot->reply_message($event->reply_token, $messages->build);
    return $self->render(json => {'status' => "200OK"});
  }
  my $messages = LINE::Bot::API::Builder::SendMessage->new;
  $messages->add_text( text => "命令を実行します!" );
  $messages->add_text( text => $reply_text );

  my $commands = {
    "電気つけて" => '{"format":"raw","freq":38,"data":[18031,...]}',
    "電気消して" => ...,
    #etc...,
  };
  # 受け取ったメッセージに対応するJSONをIRKitに送信する
  my $command = $commands->{$reply_text};
  if ($command){
    &call_irkit($command);
  } else { # ここの挙動をヘルプにするのもありだと思います
    $messages->add_text( text => "その命令は受け付けられません!" );
  }

  $bot->reply_message($event->reply_token, $messages->build);
  return $self->render(json => {'status' => "200OK"});
} => 'callback';

app->start;

LWP::UserAgentでイカリング(ニンテンドーネットワーク)の認証を突破する。

イカリングまでの認証をWWW::Mechanizeでやっていたのですが、ページ構成変わった時に対応できないよなーと思ったので、LWP::UserAgentで認証が突破できるようにしました。
参考にしたのは以下のサイトです。
<mini> Miiverse が楽しすぎて… – モノトーンの伝説日記
セッション管理をcookieでやってるようですが、ヘッダにトークンを指定してアクセスとかそういうのはできないものでしょうか。MiiverseもそうだけどAPI公開してくれると楽しいのになぁ。

手順としては以下のことをやっています。

  1. ログイン用のURLにアクセスして認証用のURLを取得
  2. 認証用のURLに認証に必要なパラメータを付与。アクセス。
  3. 返ってきたコールバックURLにアクセス

セッション管理をcookieでやっているので、cookie_jarを設定してクッキーをぱくぱくもぐもぐできるようにしています。クッキー容器っていう名称いいですね。

#!/usr/bin/perl

use warnings;
use strict;
 
use LWP::UserAgent;

my $ua = LWP::UserAgent->new;
$ua->cookie_jar({file =>"cookie.txt", autosave=>1});

my $res = $ua->post("https://splatoon.nintendo.net/users/auth/nintendo");
# 認証用のURLとパラメータを取得
my $location = $res->header("location");

# 認証に必要なパラメータの組み立て
my $dummy_url = URI->new;
$dummy_url->query_form(
"nintendo_authenticate" => "",
"nintendo_authorize" => "",
"scope" => "",
"lang" => "ja-JP" ,
"username" => "ニンテンドーネットワークのid",
"password" => "ニンテンドーネットワークのパスワード"
);

# 認証用のURLにパラメータをくっつける
my $url = URI->new($location.$dummy_url->query);
# 認証用URLにアクセスしてコールバックURLを取得
my $res_auth = $ua->post($url);
my $location_auth = $res_auth->header("location");

# コールバックURLにアクセス
$ua->get($location_auth);

# 目当てのページにアクセス
my $res_login = $ua->get("https://splatoon.nintendo.net/ranking");
print $res_login->content;