SwitchBotをLineボットで操作しよう

SwitchBotは、お手軽に入手できるスマートホームデバイスです。
購入してすぐに、AlexaスマートホームやGoogleアシスタントで使えるのは非常にうれしいです。
今回は、LINEアプリからも操作できるようにします。

LINEボットを作成して、LINEアプリから友達登録することで、お友達にチャットする感覚で、操作することができるようにします。
一方で、SwitchBotは、WebAPIを公開していただいていますので、それを利用します。
詳細は以下に記載されています。

OpenWonderLabs/SwitchBotAPI

https://github.com/OpenWonderLabs/SwitchBotAPI

LINEボットを立ち上げるため、サーバを立ち上げる必要がありますが、Node.jsサーバを利用します。
ソースコードもろもろは、GitHubに上げてあります。
poruruba/SwitchBot_SmartHome

https://github.com/poruruba/SwitchBot_SmartHome

#SwitchBotをセットアップする

以降の作業は、SwithBotアプリがスマホにインストールされ、各種スマートホームデバイスがセットアップされている状態が前提ですので、まずは何はなくとも、セットアップしておきましょう。

今回の投稿では、以下のデバイスを登録済みです。
・SwitchBotハブミニ
https://www.switchbot.jp/collections/all/products/switchbot-hub-mini

・SwitchBot温湿度計
https://www.switchbot.jp/products/switchbot-meter

また、SwitchBotハブミニは、学習機能付き赤外線リモコンでもあるため、そこにエアコンとLED電球を登録しています。
ちなみに、おそらくSwitchBotのWebAPIを利用するためには、SwitchBotハブミニも必要なはずです。

1点注意ですが、エアコンの登録はちょっとひと手間必要です。
正規のやり方はエアコンのテンプレートがあるのでそこから登録するのが正しいです。エアコンメーカを選べるので。
ですが、SwitchBot WebAPIでは、エアコンの操作は、温度やモードやOn/Offを一括設定するしかできないようなので、今回操作したいボタンを個別に登録するようにします。

登録方法を以下に示しておきます。
まずは、Hub Mini B4を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6eb6bc16-034f-19a6-448f-7a042b9fd7dc.png)

⊕リモコンを追加 を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ab6d2e53-42e0-32d0-f5f7-e49d8026fad7.png)

その他を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/28326a4d-04a0-4169-89ac-a438b583c85e.png)

リモコン名は適当でよいですが、「マイエアコン」にしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/b916a3a8-31d1-0a17-9acb-6090f711f4d8.png)

あとは、個別にリモコンボタンを登録します。ボタンの名前は適当でよいですが、この名前を後で使いますので覚えておきます。今回の例では、「暖房」、「冷房」、「除湿」、「自動」、「停止」です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f1812b5b-cb5d-a93a-6298-0852d5f986a7.png)

#SwitchBotのWebAPIを呼び出せるようにする。

WebAPIを呼び出せるようにするには、スマホのSwitchBotアプリから、ちょっとした操作をする必要があります。

プロフィールを選択してから、設定を選択します。
そのあと、アプリバージョンを10回ほどタップすると、めでたく「開発者向けオプション」が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/08a0440c-a62a-9e96-28b2-8894fad518b3.png)

そして、表示された開発者向けオプションをタップして、「トークンを取得」をタップすると、トークンが表示されます。このトークンを後で使うので覚えておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4b1c403c-5c24-ee20-7f2a-4cc737dd141d.png)

#LINEボットを立ち上げる

以下操作を順に実施します。

・LINE Developer Consoleにログイン
　https://developers.line.biz/console/
・プロバイダを選択（作成していない場合は作成）
・新規チャネルを選択
　・チャネルの種類としてMessaging APIを選択
・新規チャネル作成後
　・チャネル基本設定のチャネルシークレットをメモる
　・Messaging API設定のチャネルアクセストークン(長期)を生成して、メモる
　・Webhook設定のURLにこれから立ち上げるNode.jsサーバのURLを設定
　　　例：https://hogehoge:20443/linebot-smarthome
　・Webhookの利用をOnに設定
　・LINE公式アカウント機能の応答メッセージを無効に変更

Webhook設定したURLの検証ボタンは、これからNode.jsサーバを立ち上げるので、今は失敗します。
LINEボット立ち上げは毎度のことなので詳細は割愛しますが、以下を参考にしてください。

　[LINEボットを立ち上げるまで。LINEビーコンも。](https://qiita.com/poruruba/items/c71c96ca83c7403c2930)

#Node.jsサーバを立ち上げる

SwitchBot操作用クラスは以下の感じで作っておきました。

```js:/api/controllers/linebot-smarthome/switchbot.js
"use strict";

const base_url = "https://api.switch-bot.com/v1.0";

const { URLSearchParams } = require('url');
const fetch = require('node-fetch');
const Headers = fetch.Headers;

class SwitchBot{
    constructor(token){
      this.authorization = token;
    }

    async getDeviceList(){
      var json = await do_get_with_authorization(base_url + "/devices", null, this.authorization);
      if( json.statusCode != 100 )
        throw "statusCode is not 100";
      return json.body;
    }

    async getDeviceStatus(deviceId){
      var json = await do_get_with_authorization(base_url + "/devices/" + deviceId + "/status", null, this.authorization);
      if( json.statusCode != 100 )
        throw "statusCode is not 100";
      return json.body;
    }

    async sendDeviceControlCommand(deviceId, commandType, command, parameter ){
      var params = {
        command: command,
        parameter: parameter,
        commandType: commandType
      };
      var json = await do_post_with_authorization(base_url + "/devices/" + deviceId + "/commands", params, this.authorization);
      if( json.statusCode != 100 )
        throw "statusCode is not 100";
    }
}

module.exports = SwitchBot;

function do_get_with_authorization(url, qs, authorization) {
  const headers = new Headers({ Authorization: authorization });
  var params = new URLSearchParams(qs);

  var params_str = params.toString();
  var postfix = (params_str == "") ? "" : ((url.indexOf('?') >= 0) ? ('&' + params_str) : ('?' + params_str));
  return fetch(url + postfix, {
      method: 'GET',
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
    });
}

function do_post_with_authorization(url, body, authorization) {
  const headers = new Headers({ "Content-Type": "application/json", Authorization: authorization });

  return fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw 'status is not 200';
      return response.json();
    });
}
```

呼び出しているWebAPIは、
　https://github.com/OpenWonderLabs/SwitchBotAPI
に記載されています。
そのうち以下のAPIを使っています。
・Get device list
・Get device status
・Send device control commands

すべての呼び出しにトークンが必要であるため、コンストラクタにトークンを指定して使います。

LINEサーバからの呼び出しを受け付けるメインのソースコードを示します。

```js:/api/controllers/linebot-smarthome/index.js
'use strict';

const config = {
  channelAccessToken: '【LINEチャネルアクセストークン（長期）】',
  channelSecret: '【LINEチャネルシークレット】',
};
const SWITCHBOT_OPENTOKEN = "【SwitchBotのトークン】";
const RICHMENU_ID = "【LINEリッチメニューID】";

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const LineUtils = require(HELPER_BASE + 'line-utils');
const line = require('@line/bot-sdk');
const app = new LineUtils(line, config);
if( RICHMENU_ID )
  app.client.setDefaultRichMenu(RICHMENU_ID);

const SwitchBot = require('./switchbot');
const switchbot = new SwitchBot(SWITCHBOT_OPENTOKEN);

switchbot.getDeviceList()
.then(json =>{
  console.log(json);
});

const command_list = [
  // コマンドリスト
];

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

  var command = command_list.find(item => event.message.text.indexOf(item.disp) >= 0 );
  if( command ){
    var message = await processCommand(app, command);
    return client.replyMessage(event.replyToken, message);
  }else{
    var message = { type: 'text', text: event.message.text + ' ですね。' };
    message.quickReply = makeQuickReply(app);
    return client.replyMessage(event.replyToken, message);
  }
});

app.postback(async (event, client) =>{
  var command = command_list.find(item => item.disp == event.postback.data);
  var message = await processCommand(app, command);
  return client.replyMessage(event.replyToken, message);
});

async function processCommand(app, command){
  if( command.commandType == "Meter"){
    var json = await switchbot.getDeviceStatus(command.deviceId);
    console.log(json);
    var message = app.createSimpleResponse("温度は " + json.temperature + "℃、湿度は " + json.humidity + "% です。");
    message.quickReply = makeQuickReply(app);
    return message;
  }else{
    await switchbot.sendDeviceControlCommand(command.deviceId, command.commandType, command.command, command.parameter);
    var message = app.createSimpleResponse("送信しました。");
    message.quickReply = makeQuickReply(app);
    return message;
  }
}

function makeQuickReply(app){
  var list = [];
  command_list.forEach((item)=>{
    list.push({
      title: item.disp,
      action: {
        type: "postback",
        data: item.disp
      }
    });
  });
  var quickReply = app.createQuickReply(list);
  return quickReply;
}

exports.fulfillment = app.lambda();
```

以下のnpmモジュールを使わせていただいています。

@line/bot-sdk

https://github.com/line/line-bot-sdk-nodejs

node-fetch
ただし、バージョン2.6.6を利用します。

https://github.com/node-fetch/node-fetch

エンドポイント名は以下で指定しています。

```yaml:/api/controllers/linebot-smarthome/swagger.yaml
paths:
  /linebot-smarthome:
    post:
      x-handler: fulfillment
      parameters:
        - in: body
          name: body
          schema:
            $ref: "#/definitions/CommonRequest"
      responses:
        200:
          description: Success
          schema:
            $ref: "#/definitions/CommonResponse"
```

とりあえず、環境に合わせて以下の部分の書き換えが必要です。
メモっておいた値に書き換えます。

```js:/api/controllers/linebot-smarthome/index.js
const config = {
  channelAccessToken: '【LINEチャネルアクセストークン（長期）】',
  channelSecret: '【LINEチャネルシークレット】',
};
const SWITCHBOT_OPENTOKEN = "【SwitchBotのトークン】";
```

いったんこれで立ち上げます。
そうすると、SwitchBotに登録されているデバイスの一覧が表示されます。

以下の部分がその処理です。

```js:/api/controllers/linebot-smarthome/index.js
switchbot.getDeviceList()
.then(json =>{
  console.log(json);
});
```

私の環境の場合、以下のように表示されました。

```
{
  deviceList: [
    {
      deviceId: 'XXXXXXXXXXXX',
      deviceName: 'Meter DE',
      deviceType: 'Meter',
      enableCloudService: true,
      hubDeviceId: 'XXXXXXXXXXXX'
    },
    {
      deviceId: 'XXXXXXXXXXXX',
      deviceName: 'Hub Mini B4',
      deviceType: 'Hub Mini',
      enableCloudService: false,
      hubDeviceId: '000000000000'
    },
    {
      deviceId: 'XXXXXXXXXXXX',
      deviceName: 'Remote DC',
      deviceType: 'Remote',
      enableCloudService: false,
      hubDeviceId: '000000000000'
    },
    {
      deviceId: 'xxxxxxxxxxxxxxxxxxxxxx',
      deviceName: '屋内カメラ rg',
      deviceType: 'Indoor Cam',
      enableCloudService: false,
      hubDeviceId: '000000000000'
    }
  ],
  infraredRemoteList: [
    {
      deviceId: 'xx-xxxxxxxxxxxx-xxxxxxxx',
      deviceName: '電気',
      remoteType: 'DIY Light',
      hubDeviceId: 'XXXXXXXXXXXX'
    },
    {
      deviceId: 'xx-xxxxxxxxxxxx-xxxxxxxx',
      deviceName: 'エアコン',
      remoteType: 'Air Conditioner',
      hubDeviceId: 'XXXXXXXXXXXX'
    },
    {
      deviceId: 'xx-xxxxxxxxxxxx-xxxxxxxx',
      deviceName: 'マイエアコン',
      remoteType: 'Others',
      hubDeviceId: 'XXXXXXXXXXXX'
    }
  ]
}
```

この中で使うのは、deviceListにあるMeter DEと、infraredRemoteListにある電気とマイエアコンです。エアコンもありますが、これはエアコンとして正規の手順で登録したもので今回は使っていないです。
それぞれのdeviceIdを使います。

* SwitchBot温湿度計：Meter DEのdeviceId
* エアコン：マイエアコンのdeviceId
* LED電球：電気のdeviceId

次に、上記に合わせて変数command_listの配列を作っていきます。
出来上がりは以下のようになります。

```js:/api/controllers/linebot-smarthome/index.js
const command_list = [
  {
    disp: "電気オン",
    deviceId: "【電気のdeviceId】",
    commandType: "command",
    command: "turnOn",
    parameter: "default"
  },
  {
    disp: "電気オフ",
    deviceId: "【電気のdeviceId】",
    commandType: "command",
    command: "turnOff",
    parameter: "default"
  },
  {
    disp: "暖房オン",
    deviceId: "【マイエアコンのdeviceId】",
    commandType: "customize",
    command: "暖房",
    parameter: "default"
  },
  {
    disp: "冷房オン",
    deviceId: "【マイエアコンのdeviceId】",
    commandType: "customize",
    command: "冷房",
    parameter: "default"
  },
  {
    disp: "除湿オン",
    deviceId: "【マイエアコンのdeviceId】",
    commandType: "customize",
    command: "除湿",
    parameter: "default"
  },
  {
    disp: "エアコンオン",
    deviceId: "【マイエアコンのdeviceId】",
    commandType: "customize",
    command: "自動",
    parameter: "default"
  },
  {
    disp: "エアコンオフ",
    deviceId: "【マイエアコンのdeviceId】",
    commandType: "customize",
    command: "停止",
    parameter: "default"
  },
  {
    disp: "室温",
    deviceId: "【Meter DEのdeviceId】",
    commandType: "Meter",
  },
];
```

dispがLINEアプリ上でユーザに表示されて、操作したい時に入力してもらう文字列です。
ちなみに、マイエアコンが含まれる要素の、commandの文字列は、SwitchBotアプリから登録したボタンの名前です。

これで完成です。

#メインソースコードの補足

```js:/api/controllers/linebot-smarthome/index.js
app.message(async (event, client) =>{
```

上記の部分が、LINEアプリから入力されたメッセージを受け取る部分です。
メッセージの中に、さきほどのdispで指定した文字列があれば、それを実行します。
最後に、クイックリプライを表示します。
なぜならば、ユーザの方に、毎度dispの文字列を入力してもらうのは大変なので、クイックリプライとして候補を表示して、そこから選択してもらうようにしてあります。

```js:/api/controllers/linebot-smarthome/index.js
app.postback(async (event, client) =>{
```

上位の部分が、クイックリプライから選択されたときに呼び出されます。
event.postback.dataに選択されたdispが入っていますので、それを実行します。

```js:/api/controllers/linebot-smarthome/index.js
async function processCommand(app, command){
```

上記の関数が、実際のSwitchBotに対する操作処理の実装です。

赤外線リモコンとして登録したLED電球とエアコンは、```switchbot.sendDeviceControlCommand``` を使って操作し、温湿度計は、```switchbot.getDeviceStatus``` を使って温湿度を取得してチャットメッセージとして返しています。

LINEメッセージの処理は、ユーティリティにまとめています。```/api/helpers/line-util.js``` です。

#LINEアプリから操作する

LINE Developer Consoleで、再度さきほど作成したチャネルを選択し、Messaging APIにあるQRコードを表示して、お持ちのスマートホンのLINEアプリからスキャンして友達登録します。

後はこんな風にチャットします。チャットと言っても、クイックリプライで表示される候補をタップしているだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5d4ec713-7ae2-349f-adb7-556119c28028.png)

#(必要に応じて)LINEリッチメニューの設定

必要に応じて、LINEリッチメニューを設定してあげましょう。
以下を参考にして作成した後、以下の部分にリッチメニューIDを指定すればOKです。

```js:/api/controllers/linebot-smarthome/index.js
const RICHMENU_ID = "【LINEリッチメニューID】";
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8088f958-a8c2-4a59-3e99-7f8e1d5737a5.png)

（参考）[LINEBotのリッチメニューエディタがないので自作した](https://qiita.com/poruruba/items/adee5be5eefa83a35962)

#終わりに

ちなみに、これまで赤外線リモコンは、黒豆を使っていました。これを機にSwitchBotに切り替えました。
　 [スマートホームスキルを作る(1)：黒豆を操作するRESTful API環境を構築する](https://qiita.com/poruruba/items/b7ed1b363d3ead85227a)

以上