自分用LINE botで自宅PCをwake on lan
そんなLINEのbotを自分で作れるようになったので、何か便利なモノを作ろうと思った第1弾がタイトルの通りです。
LINEでbotに話しかけるとPCが立ち上がっているかどうかが分かって、起動させることが出来ます。
LINE botはHerokuで動かします。LINEからコマンドを受け取ったHeroku上のJava Webアプリが自宅サーバーにアクセスして、CGIで自宅LAN内のPCを操作します。セキュリティ的なアレはアレです。ご了承下さい。
解説ですが、作ったあとに改めてやり直していないので、抜けている説明もあるかもしれません。うまく行かなかったらコメント下さい。あと長いです。
環境:
自宅PC: Windows 7 x64
サーバー: Raspberry Pi 2
自宅アドレス: .tkドメイン
手順:
① 自宅サーバーのセットアップ
② LINE botアカウントの作成
③ Herokuアカウントの作成
④ Javaアプリを作成しデプロイ
では、作り方です。
① 自宅サーバーのセットアップ
前回の記事
eblbl.hatenablog.com
で使ったRaspberry PiでApacheサーバーを立てて、CGIで自宅PCの操作ができるようにします。
1. PCのIPアドレスを固定
(ググれば分かる)
192.168.xxx.xxxに固定したとします。
2. pingに応答するように設定
Windowsはデフォルトでpingに応答しないので、設定します。
https://www.google.co.jp/search?q=windows+ping+%E5%BF%9C%E7%AD%94
(ググれば分かる)
3. Raspberry Piからpingしてみる
$ ping -c 1 -W 1 192.168.xxx.xxx PING 192.168.xxx.xxx (192.168.xxx.xxx) 56(84) bytes of data. 64 bytes from 192.168.xxx.xxx: icmp_seq=1 ttl=128 time=0.*** ms --- 192.168.xxx.xxx ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.***/0.***/0.***/0.000 ms
起動していますね。
4. PCがwake on lanで起動するように設定
Windowsをwolで起動するようにします。シャットダウンからでもスリープからでも復帰できるようにしておきましょう。
www.atmarkit.co.jp
5. Raspberry Piからwolしてみる
wakeonlanをインストールします
$ sudo apt-get install wakeonlan
PCのMACアドレスを記録して、XX:XX:XX:XX:XX:XXとします。
PCをスリープもしくはシャットダウン状態にして、Raspberry Piからwolを送信します。
$ wakeonlan XX:XX:XX:XX:XX:XX
PCが起動します。
6. Apacheサーバーの設定
apache2をインストールします
$ sudo apt-get install apache2
完了すると、デフォルトのサイトが立ち上がっているはずです。
Raspberry Piで確認してみます。
$ curl localhost <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Apache2 Debian Default Page: It works</title> ...
LAN内のPCやスマホからも確認して下さい。Apacheのデフォルトページが表示されているはずです。
7. サイトの作成
デフォルトサイトをコピーして、今回使うサイトを作成します。
$ sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/wake-up-PC.conf
コピー先のファイル名は何でも大丈夫です。.confファイルを編集します。
$ sudo vim /etc/apache2/sites-available/wake-up-PC.conf
ServerNameのコメントアウトを外し、.tkドメインを設定します。
ServerName [.tkドメイン]
DocumentRootを変えたい場合は変えてください。
次の設定を追加します。
<Directory [ドキュメントルート]> SetEnv HTTP_X_FORWARDED_PROTO http SetEnv REAL_HOST_NAME [.tkドメイン] Options Indexes FollowSymLinks ExecCGI AllowOverride All Require all granted </Directory> <FilesMatch "(check|wake)"> SetHandler cgi-script </FilesMatch>
CGIの実行許可と、この後作成する"check"、"wake"ファイルの実行許可をしています。拡張子が無いファイルを実行許可したい場合は、このように個別にファイルを指定する必要があります。正規表現が使えます。
テスト用のindex.htmlを置いておきましょう。
$ echo "hello" > [ドキュメントルート]/index.html
デフォルトサイトを無効化し、今作ったサイトを有効化します。
$ sudo a2dissite 000-default $ sudo a2ensite wake-up-PC $ sudo service apache2 reload
何か追加でコマンドを実行しろというメッセージが出たら従ってください。
サイトが立ち上がっているかをチェックします。
$ curl localhost hello
CGIを書きます。今回はシェルスクリプトです。次のようなファイルをドキュメントルートに作成してください。
file: check
#!/bin/bash echo "Content-type: text/html" echo ping -c 1 -W 1 192.168.xxx.xxx > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "up" else echo "down" fi
file: wake
#!/bin/bash echo "Content-type: text/html" echo wakeonlan XX:XX:XX:XX:XX:XX > /dev/null 2>&1 echo "ok"
実行属性を与えます。
$ chmod +x [ドキュメントルート]/check $ chmod +x [ドキュメントルート]/wake
CGIを使うためのmodを有効化します。
$ sudo a2enmod cgi $ sudo service apache2 reload
LAN内のPCやスマホから、[Raspberry Piのアドレス]/checkや/wakeにアクセスしてみてください。PCの電源状態の確認と起動ができるはずです。
自宅のルーターを設定して、外からアクセスできるようにしてください。ポート変換の設定方法は「[ルーター名] ポート変換」などとググれば出てきます。
外部から特定ポート(:8080など)へのアクセスを、Raspberry PiのIPアドレスの:80にリダイレクトします。
設定後、外部の回線から/checkや/wakeへのアクセスを確認してください。これだけでも便利ですね。
② LINE botアカウントの作成
とりあえず、これに従って「3. LINE Developers の設定」まで作業を進めてください。
bita.jp
③ Herokuアカウントの作成
www.heroku.com
herokuでアカウントを作ってください。まだappは作らなくて大丈夫です。
④ Javaアプリを作成しデプロイ
GitHubアカウントがない場合には作成してください。
github.com
今回は、こちらのアプリを使わせていただきます。
github.com
これをForkあるいはCloneして、自分のリポジトリを作ります。
まずは試しに、作ったポジトリの ボタンを押してください。
Herokuの設定画面が開きます。
・「App Name (optional)」とConfig Variablesの「* APP_NAME」には同じものを入力してください。
・LINE Developersから、作成したLINE botアカウントの「Channel Secret」および「Channel Access Token」をConfig Variablesに設定してください。
Deployを押せば、とりあえずbotが動くはずです。@qrや@twtなどのコマンドが動くかを確認してください。
アプリを編集します。CallBack.javaを編集して不要なコマンド・画像とスタンプへの対応を除去し、必要なコマンドを追加します。
package ahuglajbclajep.linebot; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.DateTimeException; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Base64; import java.util.EnumMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.imageio.ImageIO; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClients; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.WriterException; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.URL; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; @WebServlet("/callback") public class CallBack extends HttpServlet { private static final String APP_NAME = System.getenv("APP_NAME"); private static final String SECRET_KEY = System.getenv("LINE_BOT_CHANNEL_SECRET"); private static final String TOKEN = System.getenv("LINE_BOT_CHANNEL_TOKEN"); private static String executeGet(String target_url) { String ret = ""; try { URL url = new URL(target_url); HttpURLConnection connection = null; try { connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { try (InputStreamReader isr = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { ret = reader.readLine(); } } } finally { if (connection != null) { connection.disconnect(); } } } catch (IOException e) { e.printStackTrace(); } return ret; } @Override public void doPost(HttpServletRequest req, HttpServletResponse res) { // 署名検証 // String sig = req.getHeader("X-Line-Signature"); byte[] reqAll; try (InputStream in = req.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { while (true) { int i = in.read(); if (i == -1) { reqAll = out.toByteArray(); break; } out.write(i); } Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256")); String csig = Base64.getEncoder().encodeToString(mac.doFinal(reqAll)); if (!csig.equals(sig)) throw(new IOException()); } catch (IOException | NullPointerException | NoSuchAlgorithmException | InvalidKeyException e) { res.setStatus(HttpServletResponse.SC_OK); return; } // 内容の解析 // ObjectMapper mapper = new ObjectMapper(); JsonNode events; try { events = mapper.readTree(reqAll).path("events"); } catch (IOException e) { res.setStatus(HttpServletResponse.SC_OK); return; } String replyMess; if ("message".equals(events.path(0).path("type").asText())) { // メッセージを受けたとき replyMess = createReply(events.path(0).path("message")); } else if ("join".equals(events.path(0).path("type").asText())){ // トークの参加を受けたとき replyMess = "\"messages\":[{\"type\":\"text\", \"text\":\"こんにちは。コマンドを受け付けます。\"}]"; } else { res.setStatus(HttpServletResponse.SC_OK); return; } // 返信する // HttpPost httpPost = new HttpPost("https://api.line.me/v2/bot/message/reply"); httpPost.setHeader("Content-Type", "application/json"); httpPost.setHeader("Authorization", "Bearer " + TOKEN); StringBuffer replyBody = new StringBuffer("{\"replyToken\":\"") .append(events.path(0).path("replyToken").asText()) .append("\",") .append(replyMess) .append("}"); httpPost.setEntity(new StringEntity(replyBody.toString(), StandardCharsets.UTF_8)); try (CloseableHttpResponse resp = HttpClients.createDefault().execute(httpPost)) { } catch (IOException e) {} res.setStatus(HttpServletResponse.SC_OK); } private String createReply(JsonNode message){ StringBuffer replyMessages = new StringBuffer("\"messages\":["); String type = message.path("type").asText(); if ("text".equals(type)) { String[] args; int argc; args = message.path("text").asText().split(" ", 2); argc = args.length; boolean replied = false; if (args[0].indexOf("PC")>=0) { String checkPC = executeGet("http://[.tkドメイン:ポート]/check"); if("up".equals(checkPC)){ replyMessages.append("{\"type\":\"text\",\"text\":\"") .append("PCは起動しています。"); }else{ replyMessages.append("{\"type\":\"text\",\"text\":\"") .append("PCは起動していません。"); } replied=true; } if (args[0].indexOf("起動")>=0) { replyMessages.append("{\"type\":\"text\",\"text\":\"") .append("起動します。"); try { // HTTP GETでPC起動 String wakePC = executeGet("http://[.tkドメイン:ポート]/wake"); } catch (ArrayIndexOutOfBoundsException | DateTimeException e) { replyMessages.append("\"},").append("{\"type\":\"text\",\"text\":\"") .append("失敗しました。"); } replied=true; } if(!replied){ replyMessages.append("{\"type\":\"text\",\"text\":\"") .append("未定義コマンドです。"); } } else if ("sticker".equals(type)) { // スタンプが送られてきたとき replyMessages.append("{\"type\":\"text\",\"text\":\"") .append("スタンプには非対応です。"); } else if ("image".equals(type)) { // 画像が送られてきたとき replyMessages.append("{\"type\":\"text\",\"text\":\"") .append("画像には非対応です。"); } replyMessages.append("\"}]"); return replyMessages.toString(); } }
編集を保存、コミット・プッシュしたら、HerokuのDeploy > Manual deploy > Deploy Branchボタンを押します。
ログが流れ、Deploy to Herokuに緑のチェックが入ったら完了です。
「PC」を含むメッセージに反応して起動状態を確認・応答します。
「起動」を含むメッセージに反応して起動させ、応答します。
書いているうちに飽きて後半は雑な解説になりました。うまくいかなかったらコメント下さい。うまくいってもコメント下さい。
第1弾はwolだけで実現できる「PCの起動」でしたが、「PCのシャットダウン・スリープ」とかも出来たらいいなと思います。
最近は暑いので、外から窓を閉めてエアコンを入れておく、とか出来たらめっちゃ快適だなと野望を膨らませてもいます。スマートホームを作ろう。