LINE Beaconを自宅に住まわせる

LINEはほんとにいろんなサービスをしていますよね。
LINEアプリを使うようになってから、便利なことが増えました。そして、そんなたくさんのサービスをWebの技術の勉強のネタに使わせてもらってます。

今回は、LINE Beaconで遊びたいと思います。
どんなことをするかというと、自宅に帰ると「おかえりなさい。今日も一日お疲れさま。」とLINE通知してくれたり、自宅を出ると「いってらっしゃい！今日の天気は晴れだよ。」なんてことをLINE通知してくれるようにしたいと思います。

使うのは、以下の通りです。

[ハードウェア]

* micro:bit

BLEビーコン端末として使います。LINE Beaconの仕様を満たすものであれば、なんでもよいですが、ちょうど手元に使っていないmicro:bitがあったので、それを使いました。BLEのアドバタイズデータを自由にできるのであれば、なんでもLINE Beacon対応になれそうです。

[ソフトウェア]

* LINE Beacon
https://developers.line.biz/ja/docs/messaging-api/using-beacons/
今回の主題であり、LINE Beaconを検出してLINEボットに通知してくれるサービスです。

* 気象庁の天気予報情報を XML で配信
https://www.drk7.jp/weather/
天気予報を教えてくれます。ほかにもいろんな種類がありますが、降水確率も教えてくれたのでこれを選びました。

* docomo 自然対話(雑談対話)
https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=natural_dialogue&p_name=api_4_usage_scenario#tag01
Botっぽく振舞えるようにしたくて、これで簡単な話し相手をしてくれます。

[RESTful実行環境]

そして、ハードウェアからの通知を受け取って、ソフトウェアを組み合わせてLINE通知を返すRESTful実行環境が必要です。SSLサーバである必要があります。

(2020/2/11 修正)
・ハードウェアIDの払い出しのURLを追記しました。（こめんといただきありがとうございました！）
・docomo自然対話(雑談対話)が使えなくなるとのことで、UserLocalの人工知能チャットボットも併記しました。

# micro:bitをLINE Beacon化する

以下の方が、micro:bitをLINE Beacon化してくださっています（ありがとうございます！）

① [LINE Simple Beacon with BBC micro:bit](https://github.com/Pizayanz/arduino-LINESimpleBeacon)
② [pxt-linebeacon](https://github.com/pizayanz/pxt-linebeacon)

丁寧に説明していただいていますので、特に問題なくできると思います。
説明の中でも説明がありますが、サンプルプログラムを実行(コンパイル含む)をする前に、これから使うLINE Beacon端末にユニークな番号を付与する必要があります。
LINE Beacon端末には、どのLINEアプリにも反応するのではなく、いづれかのLINEボット(Messaging API)に紐づけられ、そのボットを友達にすることで、LINEアプリにLINE Beaconからの通知が行くようになります。

ということで、まずはボットを作成しておきます。
以下で、プロバイダを作成します。（未作成の場合）
　https://developers.line.biz/console/

それから、Messaging APIの新規チャネル(ようするにLINEボット)を作成します。（未作成の場合）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/c691dad7-701e-419b-0b56-8b78d658e1ea.png)

たとえば、「TestBeaconBot」という名前を付けます。
プランはフリーを選択しました。

そして、いよいよ、ユニークな番号であるハードウェアIDを払い出します。
　https://admin-official.line.me/beacon/register#/
または
　https://manager.line.biz/beacon/register

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/cf83776f-988e-6a55-47c5-94c952ffeb0f.png)

LINE Simple BeaconのハードウェアIDの払い出しを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/476307a9-2c84-48e1-4c3b-96d01b1c3bfb.png)

アカウント一覧から、先ほど作成したボットを選択します。
ハードウェアID発行 ボタンを押下すると、５バイトの１６進数文字列が表示されました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ca57df6b-b716-315c-62c3-0a23fd90d1cd.png)

[Arduino IDEの場合]

①を採用した場合です。

使用させていただいたライブラリはこちらです。

LINE Simple Beacon with BBC micro:bit (pizayanz/arduino-LINESimpleBeacon)
　https://github.com/pizayanz/arduino-LINESimpleBeacon

依存するライブラリは以下です。上記のライブラリのReadmeに丁寧に記載していただいています。
・Nordic Semiconductor nRF5 based boards 
・BLEPeripheral by Sandeep Mistry

このハードウェアIDを、Arduino IDEから、以下のソースの該当部分に入力して、マイコンボード(micro:bit)に書き込みを行います。

```LINESimpleBeacon.ino
// Copyright (c) Pizayanz. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#include <LINESimpleBeacon.h>

// https://admin-official.line.me/beacon/register#/
// LINEから払い出されたhw idを2桁区切りにして0x__の部分に入れる
unsigned char hwid[5] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX};

LINESimpleBeacon lineSimpleBeacon = LINESimpleBeacon(hwid);

void setup() {
  Serial.begin(9600);

  // deviceMessage は 13文字まで
  lineSimpleBeacon.begin("foobarbaz");

  Serial.println(F("Line Simple Beacon"));
}

void loop() {
  lineSimpleBeacon.loop();
}
```
※書き込むときのボード設定は以下のようにしてください。(忘れやすいので)
・ボード：BBC micro:bit
・Softdevice：S110またはS130

[MakeCodeの場合]

②を採用した場合です。

Microsoft MakeCode for micro:bit
　https://makecode.microbit.org/

LINE Beacon for micro:bit (pizayanz/pxt-linebeacon)
　https://github.com/pizayanz/pxt-linebeacon

ハードウェアIDを「LINE Beacon start HWID is」のところに入力しておきます。
ボタンAを押すと10秒間だけアドバタイズされるようにしています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/7176893f-3d57-7eb5-3653-060469f9604d.png)

以上、書き込みが終わったら、電源を入れなおせば、LINE Beaconとして動作しているはずです。
Androidアプリ「nRF Connect」などを使えば、SCANNERに引っかかっているかと思います。（ちょっと見方が難しいですが）


※最近MakeCodeで書き込んだらなぜかエラーが発生するようになったなあ。。。

# LINE Beaconからの通知を受け取る

実はもうこれで、紐づけたボット（TestBeaconBot）を友達に加えれば、LINE Beaconからの通知が出るのですが、受け取る側を用意していませんでした。

そのためには、TestBeaconBotのチャネル基本設定で設定しておく必要があります。
Channel Secret は後で使いますので、覚えておきます。
アクセストークン（ロングターム）を使いますので、まだ発行していなければ発行して、それを覚えておきます。

Webhook送信は「利用する」に編集しておきます。
Webhook URLは、これから立ち上げるRESTful実行環境のエンドポイントのURLを指定します。
自動応答メッセージは「利用しない」に設定しておきましょう。

編集が終わったら、QRコード(アプリ検証時や友達に紹介する際にご活用ください）をAndroidからスキャンして、このボットを友達登録しておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ec34d6dd-3b37-620f-96b0-7f55791b25c3.png)

# ちょっと横道にそれて、docomo自然対話(雑談対話)のアプリ登録

ボットっぽく会話をしたいため、docomo 自然対話(雑談対話)を利用します。

(参考)
https://dev.smt.docomo.ne.jp/?p=about.index

以下のページから、「申請する」に進み、アカウント登録とアプリ登録を済ませます。
それにより、API KEYが払い出されます。

次に、以下のページにある通り、雑談対話のためのユーザ登録をします。

https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=natural_dialogue&p_name=api_4_user_registration#tag01

Postmanやcurlを使って、実施し、アプリケーションIDを取得しておきます。

# RESTful実行環境の構築

これで準備が整いましたので、LINE Beaconからの通知を受け付けるRESTful実行環境を構築します。
毎度の通り、Swaggerを使ってRESTful実行環境を構築します。

以下をご参考にしてください。
[SwaggerでRESTful環境を構築する](https://qiita.com/poruruba/items/72d8c85f8923bc78b918)

Swagger定義ファイルは以下の通りです。
適当に「/linebeacon」としています。TestBeaconBotのチャネル基本設定で指定したWebhook URLと同じにしてください。

```swagger.yaml
  /linebeacon:
    post:
      x-swagger-router-controller: routing
      operationId: linebeacon
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object
```

以下が、実装部分です。解説は後程。

```index.js
const config = {
    channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
    channelSecret: process.env.LINE_CHANNEL_SECRET,
};
const DOCOMO_API_KEY = process.env.DOCOMO_API_KEY || 【docomo Developer supportのアプリのAPI KEY】;
const DOCOMO_APP_ID = process.env.DOCOMO_APP_ID || 【docomo Developer supportのアプリのID】;

const LineUtils = require('../../helpers/line-utils');
const app = new LineUtils(config);
const fetch = require('node-fetch');

app.message(async (event, client) =>{
    console.log(event);

    var message = await do_talk(event.message.text);
    const echo = { type: 'text', text: message };
    return client.replyMessage(event.replyToken, echo);
});

app.beacon(async (event, client) =>{
    console.log(event);

    var message;
    if( event.beacon.type == 'enter'){
        message = 'おかえりなさい。';
        message += '今日も一日お疲れさまでした。';
    }else{
        message = 'いってらっしゃい！';
        var weather = await do_get_wether(14);
        var weather_message = parse_weather(weather.pref.area['東部'].info[0]);
        message += '\n横浜の天気は、' + weather_message.weather + '\n' + weather_message.rainfallchance;
    }
    const echo = { type: 'text', text: message };
    return client.replyMessage(event.replyToken, echo);
});

/* location: 13:東京、14:神奈川 */
function do_get_wether(location){
    return fetch('https://www.drk7.jp/weather/json/' + location + '.js', {
        method : 'GET'
    })
    .then((response) => {
        return response.text();
    })
    .then(text =>{
        text = text.trim();
        if( text.startsWith('drk7jpweather.callback(') )
            text = text.slice(23, -2);
        return JSON.parse(text);
    });
}

function parse_weather(info){
    var weather = info.weather + ' です。';
    var rainfallchance = "降水確率は、" +
    "0-6時 " + info.rainfallchance.period[0].content + info.rainfallchance.unit +
    "、6-12時 " + info.rainfallchance.period[1].content + info.rainfallchance.unit +
    "、12-18時 " + info.rainfallchance.period[2].content + info.rainfallchance.unit +
    "、17-24時 " + info.rainfallchance.period[3].content + info.rainfallchance.unit +
    " です。";

    return { weather: weather, rainfallchance: rainfallchance };
}

function do_talk(message){
    var url = 'https://api.apigw.smt.docomo.ne.jp/naturalChatting/v1/dialogue?APIKEY=' + DOCOMO_API_KEY;
    var body = {
        language: 'ja-JP',
        botId: 'Chatting',
        appId: DOCOMO_APP_ID,
        voiceText: message
    };
    return do_post(url, body)
    .then(result =>{
        console.log(result);
        return result.systemText.expression;
    });
}

function do_post(url, body){
    return fetch(url, {
        method : 'POST',
        body : JSON.stringify(body),
        headers: { "Content-Type" : "application/json; charset=utf-8" } 
    })
    .then((response) => {
        if(!response.ok)
            throw "status is not 200.";
        return response.json();
    });
}

exports.handler = app.lambda();
```
ユーティリティも示しておきます。line-utils.jsとresponse.jsです。

```line-utils.js
'use strict';

const line = require('@line/bot-sdk');
const Response = require('./response');

class LineUtils{
    constructor(config){
        this.client = new line.Client(config);
        this.map = new Map();
    }

    message(handler){
        this.map.set('message', handler);
    }

    follow(handler){
        this.map.set('follow', handler);
    }

    unfollow(handler){
        this.map.set('unfollow', handler);
    }

    join(handler){
        this.map.set('join', handler);
    }

    leave(handler){
        this.map.set('leave', handler);
    }

    memberJoined(handler){
        this.map.set('memberJoined', handler);
    }

    memberLeft(handler){
        this.map.set('memberLeft', handler);
    }

    postback(handler){
        this.map.set('postback', handler);
    }

    beacon(handler){
        this.map.set('beacon', handler);
    }

    accountLink(handler){
        this.map.set('accountLink', handler);
    }

    things(handler){
        this.map.set('things', handler);
    }

    lambda(){
        return async (event, context, callback) => {
            var body = JSON.parse(event.body);

            return Promise.all(body.events.map((event) =>{
                if( (event.type == 'message') &&
                     (event.replyToken === '00000000000000000000000000000000' || event.replyToken === 'ffffffffffffffffffffffffffffffff' ))
                    return;

                var handler = this.map.get(event.type);
                if( handler )
                    return handler(event, this.client);
                else
                    console.log(event.type + ' is not defined.');
            }))
            .then((result) =>{
//                console.log(result);
//                return new Response(result);
                return new Response({});
            })
            .catch((err) => {
                console.error(err);
                const response = new Response();
                response.set_error(err);
                return response;
            });
        }
    }
};

module.exports = LineUtils;
```

```response.js
class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
    }

    set_body(content){
        this.body = JSON.stringify(content);        
    }
    
    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;
```

以下のnpmモジュールを使います。

* @line/bot-sdk
* node-fetch

また、各自で、以下の環境変数を設定する必要があります。

LINE_CHANNEL_ACCESS_TOKEN
　さきほど覚えておいたLINEのアクセストークン（ロングターム）のことです。
LINE_CHANNEL_SECRET
　さきほど覚えておいたLINEのChannel Secretのことです。
DOCOMO_API_KEY
　さきほど覚えておいたdocomo Developer supportのAPI_KEYです。
DOCOMO_APP_ID
　さきほど覚えておいたdocomo Developer supportのアプリケーションIDのことです。

line-utils.js は、LINE BeaconからくるMessaging API形式の通知の受付を、Actions on Google SDKっぽくするために作りました。

以下のURLで示す「イベントのタイプを表す識別子」ごとに、受信後の処理を記述できるようにしています。

https://developers.line.biz/ja/reference/messaging-api/#anchor-ff6d9a0f9685bb1dfdde749b9044d243cadd542e

例えば、メッセージイベントの場合には、

> app.message( (event, client) =>{
> 　/* 処理内容 */
> });

と定義します。docomo 自然対話(雑談対話) では、ここに記述しています。
　function do_talk(message)
に実装をまとめています。

そして、LINE Beaconからの通知は、ビーコンイベントであり、

> app.beacon( (event, client) =>{
> 　/* 処理内容 */
> });

という感じで実装します。
event.beacon.type が 'enter' の場合には、ビーコンの受信圏内に入ったことを示し、'leave' の場合には、ビーコンの受信圏外に出たことを示します。（このleaveは廃止予定だそうです。残念！！！）

'leave' のときには、天気予報も返しています。
　function do_get_wether(location)
で、天気情報を取得して、
　function parse_weather(info)
で欲しい情報を抽出しています。
locationには、都市の番号を指定するのですが、神奈川が14で、東京が13でした。
以下のページに行くとたくさんリンクがありますが、所望の地域のURLから判断してください。今回は、神奈川を選択し、取得した情報のうちの東部地方(横浜近辺)を使っています。

気象庁の天気予報情報を XML で配信
　https://www.drk7.jp/weather/

# 動作確認

さっそく、RESTful実行環境を起動しましょう。
スマホのBluetoothは有効になっていますか？
そして、micro:bitの電源をOnにしましょう。

以下のようなLINE通知が来ましたでしょうか？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/e5bf0068-06f3-bb8d-2a12-b6aafdf5d0e5.png)

今度は、micro:bitの電源をOffにします。今度は以下のようなLINE通知が来ましたでしょうか？（ちょっと1分ほど時間がかかります）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/7cfdb10c-b4f3-7cde-76cb-3c517ff7306d.png)

（わかりやすくするために、Botの画像を初〇ミ〇にしています。。。）

適当に話しかけると、適当に返してくれますよ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/64d8f416-683a-a26f-0f81-2c0a57d6f356.png)

帰宅時、出社時に声をかけて欲しいことがほかにもありますか？
世の中には便利なWeb APIがたくさんありますので、いろいろ組み合わせてみると、ボットに愛着がわいてくるかもしれませんね。

# (追記) UserLocalの人工知能チャットボット(chatbot)に置き換える

docomo自然対話(雑談対話)が使えなくなるそうで、UserLocalのチャットボットに変えてみます。

まずは、以下から、個人開発者向け：ボットAPI利用申請をします。

人工知能チャットボット(chatbot)
　https://ai.userlocal.jp/document/free/top/

そうすると、API Keyが払い出されますので、それをメモっておきます。

あとは、関数do_talkを以下に置き換えれば、完成です。

```js:index.js
const USERLOCAL_API_KEY = '【UserLocalのAPIKey】';

function do_talk(message){
	var body = {
		message: message,
		key: USERLOCAL_API_KEY,
	};
	return do_post('https://chatbot-api.userlocal.jp/api/chat', body)
	.then(json =>{
		console.log(json);
		return json.result;
	});
}
```

USERLOCAL_API_KEYのところは、取得したAPI Keyに置き換えてください。

以上です。