スマートホームスキルを作る(1)：黒豆を操作するRESTful API環境を構築する

今回から2回の投稿に分けて、黒豆を使ったAlexaスマートホームスキルを作りたいと思います。

黒豆とは、世の中的には、Broadlink社製の RM Mini 3 のことを指しています。
赤外線の学習リモコンでなおかつWifiから制御できます。
Wifiで制御できるので、Alexaから「寝室の照明を消して」というようにスマートホーム化にはぴったりです。

ちょっと内容が多そうだったので2回に分けます。
1回目の今回は、Alexa対応は置いておいて、まずは単独でWebブラウザから、黒豆に赤外線を学習させ、学習した赤外線を発信するまでを行います。RESTful環境で構築しておき、2回目の投稿に備えます。
2回目の投稿は、Alexaスマートホームスキルを作成し、そのスキルから前回作成したRESTful環境と連携させることでAlexaでスマートホーム化します。

以下のハードウェアを使います。

* Raspberry Pi（ラズパイ必須というわけではないのですが、CPU温度もスマートホーム化して取得できるようにしたかったためです）
* 黒豆
* Echo dot

[2018.11.21 修正]
結局のところ、broadlinkjs-rmをカスタマイズしようとしたのですが、ほぼそのままだったので、オリジナルそのままを使うようにしました。
また、swagger.yamlのモデル定義をきちんと定義するようにしました。

ちなみに第2回投稿は済んでいてこちらです。
[スマートホームスキルを作る(2)：いよいよスマートホームスキルを作成する](https://qiita.com/poruruba/items/5e31b82bfbdeef20519d)

# 黒豆のネットワーク設定

まずは黒豆を設置される場所のWiFi環境に接続できる状態にします。
GooglePlayで複数のアプリがありますが、たとえば、「BroadLink e-Control」を使います。
実は、上記アプリでは、インストール途中で失敗します。ですが、必要なのは、

* 黒豆のWiFiセットアップが完了していること
* 黒豆のMACアドレスがわかること

なので、セットアップ途中で表示されるMACアドレスがわかればそれで充分です。アプリは削除していただいて大丈夫です。
もし、同じネットワーク内に黒豆が1台しかないのであれば、MACアドレスは認識する必要はありません。

# 黒豆操作用Node.jsモジュールの作成

黒豆の操作には以下を利用させていただきました。

https://github.com/lprhodes/broadlinkjs-rm

これをちょっと使いやすくするラッパーモジュールを作成しました。

rmmini3/rmmini3.js

```rmmini3.js
const Broadlink = require('broadlinkjs-rm');

class RMMini3{
    constructor(){
        this.broadlink = new Broadlink();
    }

    async find_device(tmout, mac){
        this.broadlink.discover();

        if( mac )
            mac = mac.replace(/:/g, '');
        return new Promise((resolve, reject) =>{
            let settmout = setTimeout(() =>{
                reject('device not found');
            }, tmout);
            this.broadlink.on('deviceReady', (device) =>{
                if( mac && mac != device.mac.toString('hex') )
                    return;
                clearTimeout(settmout);
                return resolve(device);
            });
        });
    }

    async send_irdata(device, irdata){
        device.sendData(new Buffer(irdata, 'hex'));
    }

    async learning(device, timeout, interval){
        return new Promise((resolve, reject) =>{
            var _interval = this.waitCheckData(device, timeout, interval, () =>{
                device.removeListener('rawData', onRawData);
                device.cancelLearn();
                return reject('no irdata');
            });

            var onRawData = (message) => {
                clearInterval(_interval);
                device.removeListener('rawData', onRawData);
                device.cancelLearn();

                var irdata = message.toString('hex');
                return resolve(irdata);
            };

            device.on('rawData', onRawData );
            device.enterLearning();
        });
    };

    waitCheckData(device, timeout, interval, func_timeout ){
        let _interval = setInterval(()=>{
            console.log('checkData');
            device.checkData();

            timeout -= interval;
            if( timeout <= 0 ){
                clearInterval(_interval);
                func_timeout();
            }
        }, interval);

        return _interval;
    }
}

module.exports = new RMMini3();
```

以下、使い方です。

requireです。
> const rmmini3 = require('./rmmini3');

黒豆を同一ネットワーク上から検索を開始します。見つけるまでの待ち時間を指定します。
> rm = await rmmini3.find_device(10000);

赤外線データを黒豆で受信します。待ち受ける全体時間と、定期的な受信チェック時間です。
> var irdata = await rmmini3.learning(rm, 10000, 1000);

赤外線データを黒豆から発信します。
> rmmini3.send_irdata(rm, irdata);

# RESTful実行環境の構築

ということで、さきほど作成したモジュールをRESTful受付から呼び出せるようにします。

Swagger定義ファイルを示します。

```swagger.yaml
swagger: '2.0'
info:
  version: 'first version'
  title: OpenID Connect Server
host: localhost:10011
basePath: /

schemes:
  - http
  - https

consumes:
  - application/json
produces:
  - application/json

securityDefinitions:
  basicAuth:
    type: basic
  tokenAuth:
    type: apiKey
    name: Authorization
    in: header
#  jwtAuth:
#    authorizationUrl: ""
#    flow: "implicit"
#    type: "oauth2"
#    x-google-issuer: "https://cognito-idp.ap-northeast-1.amazonaws.com/【CognitoのプールID】"
#    x-google-jwks_uri: "https://cognito-idp.ap-northeast-1.amazonaws.com/【CognitoのプールID】/.well-known/jwks.json"
#    x-google-audiences: "【CognitoのアプリクライアントID】"

paths:
  /swagger:
    x-swagger-pipe: swagger_raw

  /get-cputemp:
    post:
      x-swagger-router-controller: routing
      operationId: get-cputemp
      responses:
        200:
          description: Success
          schema:
            type: object

  /rm-get-list:
    post:
      x-swagger-router-controller: routing
      operationId: rm-get-list
      responses:
        200:
          description: Success
          schema:
            type: object

  /rm-learn:
    post:
      x-swagger-router-controller: routing
      operationId: rm-learn
      responses:
        200:
          description: Success
          schema:
            type: object

  /rm-create:
    post:
      x-swagger-router-controller: routing
      operationId: rm-create
      parameters:
        - in: body
          name: RmCreate
          required: true
          schema:
            $ref: '#/definitions/RmCreateRequest'
      responses:
        200:
          description: Success
          schema:
            type: object

  /rm-update:
    post:
      x-swagger-router-controller: routing
      operationId: rm-update
      parameters:
        - in: body
          name: RmUpdate
          required: true
          schema:
            $ref: '#/definitions/RmUpdateRequest'
      responses:
        200:
          description: Success
          schema:
            type: object

  /rm-fire:
    post:
      x-swagger-router-controller: routing
      operationId: rm-fire
      parameters:
        - in: body
          name: RmFire
          required: true
          schema:
            $ref: '#/definitions/RmFireRequest'
      responses:
        200:
          description: Success
          schema:
            type: object

  /rm-delete:
    post:
      x-swagger-router-controller: routing
      operationId: rm-delete
      parameters:
        - in: body
          name: RmDelete
          required: true
          schema:
            $ref: '#/definitions/RmDeleteRequest'
      responses:
        200:
          description: Success
          schema:
            type: object

definitions:
  Empty:
    type: "object"
    title: "Empty Schema"

  RmCreateRequest:
    type: object
    required:
    - friendlyName
    properties:
      friendlyName:
        type: string
      description:
        type: string
      irdata_on:
        type: string
      irdata_off:
        type: string

  RmUpdateRequest:
    type: object
    required:
    - endpointId
    properties:
      endpointId:
        type: string
      friendlyName:
        type: string
      description:
        type: string
      irdata_on:
        type: string
      irdata_off:
        type: string

  RmFireRequest:
    type: object
    required:
    - endpointId
    - type
    properties:
      endpointId:
        type: string
      type:
        type: string
        enum:
          - irdata_on
          - irdata_off

  RmDeleteRequest:
    type: object
    required:
    - endpointId
    properties:
      endpointId:
        type: string
```

RESTful実況環境では、学習した赤外線データをリストとして保持しておき、保持した赤外線データを送信するようにします。

RESTful呼び出しのエンドポイントは以下の通りです。

/rm-get-list
　保持しておいた赤外線データのリストを取得します。そうそう変えるわけではないので、保持先としてファイルとしました。
/rm-create
　リストとして保持する赤外線データを作成します。赤外線データの学習に黒豆を使います。
　作成時にendpointIdを自動生成していますが、他と被らないようにuuidを生成し割り当てています。
/rm-update
　リストとして保持していた赤外線データの内容を更新します。
/rm-delete
　リストとして保持しておいた赤外線データを削除します。
/rm-fire
　リストとして保持しておいた赤外線データを黒豆から発信します。

以下が、RESTful呼び出しの実装です。

rmmini3/index.js

```index.js
const Response = require('../../helpers/response');
const fs = require('fs');

const uuidv4 = require('uuid/v4');
const rmmini3 = require('./rmmini3');
const jwt_decode = require('jwt-decode');

const FILEPATH = process.env.RMMINI3_FILE_PATH || './data/list.json';

var rm = null;

try {
    fs.statSync(FILEPATH);
} catch (error) {
    fs.writeFileSync(FILEPATH, JSON.stringify([]), 'utf8');
}

exports.handler = async (event, context, callback) => {
    switch( event.path ){
        case '/rm-get-list': {
            var list = JSON.parse(fs.readFileSync(FILEPATH, 'utf8'));
            var response = new Response({ list: list });
            callback(null, response);
            break;
        }
        case '/rm-create' : {
            var body = JSON.parse(event.body);

            var friendlyName = body.friendlyName;
            var description = body.description ? body.description : "";
            var irdata_on = body.irdata_on ? body.irdata_on : "";
            var irdata_off = body.irdata_off ? body.irdata_off : "";

            var list = JSON.parse(fs.readFileSync(FILEPATH, 'utf8'));
            var device = { endpointId: uuidv4(), friendlyName: friendlyName, description: description, irdata_on: irdata_on, irdata_off: irdata_off};
            list.push(device);
            fs.writeFileSync(FILEPATH, JSON.stringify(list), 'utf8');

            var response = new Response(device);
            callback(null, response);

            break;
        }
        case '/rm-update' : {
            var body = JSON.parse(event.body);

            var endpointId = body.endpointId;
            var friendlyName = body.friendlyName;
            var description = body.description;
            var irdata_on = body.irdata_on;
            var irdata_off = body.irdata_off;

            var list = JSON.parse(fs.readFileSync(FILEPATH, 'utf8'));
            var index = search_list(list, endpointId);
            if( index < 0 ){
                callback('device not foudn');
                return;
            }
            if( friendlyName ) list[index].friendlyName = friendlyName;
            if( description ) list[index].description = description;
            if( irdata_on ) list[index].irdata_on = irdata_on;
            if( irdata_off ) list[index].irdata_off = irdata_off;
            fs.writeFileSync(FILEPATH, JSON.stringify(list), 'utf8');

            var response = new Response(list[index]);
            callback(null, response);
            break;
        }
        case '/rm-delete': {
            var body = JSON.parse(event.body);

            var endpointId = body.endpointId;

            var list = JSON.parse(fs.readFileSync(FILEPATH, 'utf8'));
            var index = search_list(list, endpointId);
            if( index < 0 ){
                callback('device not foudn');
                return;
            }
            list.splice(index, 1);
            fs.writeFileSync(FILEPATH, JSON.stringify(list), 'utf8');

            var response = new Response({ endpointId: endpointId });
            callback(null, response);
            break;
        }
        case '/rm-learn': {
            try{
                if( !rm )
                    rm = await rmmini3.find_device(10000);
                var irdata = await rmmini3.learning(rm, 10000, 1000);
                var response = new Response({ irdata: irdata});
                callback(null, response);
            }catch(error){
                callback(error);
            }
            break;
        }
        case '/rm-fire':{
            var token = event.headers.Authorization;
            if( token )
                console.log(jwt_decode(token));

            var body = JSON.parse(event.body);

            try{
                if( !rm )
                    rm = await rmmini3.find_device(10000);
        
                var endpointId = body.endpointId;
                var type = body.type;

                var list = JSON.parse(fs.readFileSync(FILEPATH, 'utf8'));
                if( list.length == 0 ){
                    callback('list not found');
                    return;
                }
                var index = search_list(list, endpointId);
                if( index < 0 ){
                    callback('device not found');
                    return;
                }

                var irdata;
                if( type == 'irdata_on' )
                    irdata = list[index].irdata_on;
                else
                    irdata = list[index].irdata_off;
                if( irdata == "" ){
                    callback('irdata not defined');
                    return;
                }

                rmmini3.send_irdata(rm, irdata);

                var response = new Response({ endpointId: endpointId, type: type});
                callback(null, response);
            }catch(error){
                callback(error);
            }
            break;
        }
    }
};

function search_list(list, endpointId){
    for( var i = 0 ; i < list.length ; i++ ){
        if( list[i].endpointId == endpointId )
            return i;
    }

    return -1;
}
```

ヘルパーも示しておきます。

../helpers/response.js

```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;
```

便宜上、express型のインタフェースではなく、Lambda型のインタフェースで作成したので、以下の変換をかまします。（中身は適当ですみません）
みにくくてすみませんが、以下の関係です。

> app.js→routing.js→rmmini3/index.js

```routing.js
'use strict';

/* 関数を以下に追加する */
const func_table = {
  "get-cputemp" : require('./get_cputemp').handler,

  "rm-get-list" : require('./rmmini3').handler,
  "rm-learn" : require('./rmmini3').handler,
  "rm-create" : require('./rmmini3').handler,
  "rm-update" : require('./rmmini3').handler,
  "rm-delete" : require('./rmmini3').handler,
  "rm-fire" : require('./rmmini3').handler,
};
/* ここまで */

var exports_list = {};
for( var operationId in func_table ){
  exports_list[operationId] = routing;
}

module.exports = exports_list;

function routing(req, res) {
//    console.log(req);
  
  var operationId = req.swagger.operation.operationId;
  console.log('[' + operationId + ' calling]');

  try{
    var event;
    var func;
    if( func_table.hasOwnProperty(operationId) ){
      event = {
        headers: req.headers,
        body: JSON.stringify(req.body),
        path: req.swagger.apiPath,
        httpMethod: req.method,
        queryStringParameters: req.query,
        requestContext: ( req.requestContext ) ? req.requestContext : {}
      };
    
      if( event.headers['content-type'] )
        event.headers['Content-Type'] = event.headers['content-type'];
      if( event.headers['authorization'] )
        event.headers['Authorization'] = event.headers['authorization'];
      if( event.headers['user-agent'] )
        event.headers['User-Agent'] = event.headers['user-agent'];

      func = func_table[operationId];
      res.func_type = "normal";
    }else{
      console.log('can not found operationId: ' + operationId);
      return_error(res, new Error('can not found operationId'));
      return;
    }

    //  console.log(event);

    var task = func(event, null, (error, response) =>{
      if( error )
        return_error(res, error);
      else
        return_response(res, response);
    });
  }catch(err){
    console.log('error throwed: ' + err);
    return_error(res, err);
  }
}

function return_error(res, err){
  res.status(500);
  res.json({ errorMessage: err.toString() });
}

function return_response(res, ret){
  if( ret.statusCode )
    res.status(ret.statusCode);
  for( var key in ret.headers )
    res.set(key, ret.headers[key]);

//  console.log(ret.body);

  if (!res.get('Content-Type'))
    res.type('application/json');

  if( ret.body )
    res.send(ret.body);
  else
    res.send("{}");
}
```

赤外線情報を保持しておくファイル名も指定します。JSON形式なので、拡張子は.jsonが良いです。

> RMMINI3_FILE_PATH="./data/list.json"

実はRESTful呼び出しのエンドポイントに以下もあります。

/get-cputemp
　これは黒豆とは関係ないのですが、ラズパイのCPUの温度を返します。
　Alexaスマートホーム化するデバイスの種類をもう一つ増やすためです。ですのでこれは必須ではないです。

get-cputemp/index.js

```index.js
var fs = require('fs');
const Response = require('../../helpers/response');
const jwt_decode = require('jwt-decode');

exports.handler = async (event, context, callback) => {
    var token = event.headers.Authorization;
    if( token )
        console.log(jwt_decode(token));
    
    return new Promise((resolve, reject) =>{
        fs.readFile('/sys/class/thermal/thermal_zone0/temp', function(err, data){
            if( err ){
                console.log('error: ', err);
                callback(err);
                return resolve();
            }
            var temp = data.toString().trim();
            callback(null, new Response({temp: parseInt(temp) / 1000.0 }));
            return resolve();
        });
    })
};
```

RESTful実行環境の構築には以下を参考にしてください。

[SwaggerでRESTful環境を構築する](https://qiita.com/poruruba/items/72d8c85f8923bc78b918)

ちなみに、以下のモジュールを使っています。

    "cors": "^2.8.4",
    "dotenv": "^6.1.0",
    "jwt-decode": "^2.2.0",
    "request": "^2.88.0",
    "broadlinkjs-rm": "^0.6.0",
    "swagger-express-mw": "^0.1.0",
    "uuid": "^3.3.2"

# Webページの作成

これだけだと、RESTful呼び出しが必要で、ちょっと手軽に扱うには面倒です。Webページを用意しました。
publicフォルダ配下にでも置いてください。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>スマートホーム管理</title>

  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>スマートホーム管理</h1>

        <div class="panel panel-primary">
            <div class="panel-heading">
                <div class="panel-title"><a data-toggle="collapse" href="#irdata-register">登録</a></div>
            </div>
            <div class="panel-collapse collapse" id="irdata-register">
                <div class="panel-body">
                    <div class="form-group">
                        <label>friendlyName</label>
                        <input type="text" class="form-control" v-model="irdata_register.friendlyName">
                    </div>
                    <div class="form-group">
                        <label>description</label>
                        <input type="text" class="form-control" v-model="irdata_register.description">
                    </div>
                    <div class="form-group">
                        <label>irdata_on</label>
                        <textarea class="form-control" rows="4" v-model="irdata_register.irdata_on"></textarea>
                        <button class="btn btn-default" v-on:click="irdata_learn_register_on()">赤外線受信(ON)</button> 
                    </div>
                    <div class="form-group">
                        <label>irdata_off</label>
                        <textarea class="form-control" rows="4" v-model="irdata_register.irdata_off"></textarea>
                        <button class="btn btn-default" v-on:click="irdata_learn_register_off()">赤外線受信(OFF)</button> 
                    </div>

                    <button class="btn btn-primary" v-on:click="irdata_register_call()">登録実行</button> 
                    <button class="btn btn-default" data-toggle="collapse" href="#irdata-register">閉じる</button>
                </div>
            </div>
        </div>

        <div class="panel panel-primary">
            <div class="panel-heading">
                <div class="panel-title"><a data-toggle="collapse" href="#irdata-update">更新</a></div>
            </div>
            <div class="panel-collapse collapse" id="irdata-update">
                <div class="panel-body">
                    <button class="btn btn-primary" v-on:click="irdata_update_delete(irdata_update.endpointId)">削除実行</button><br>
                    <br>
                    <div class="form-group">
                        <label>endpointId</label> {{irdata_update.endpointId}}
                    </div>
                    <div class="form-group">
                        <input type="checkbox" v-model="irdata_check.friendlyName">
                        <label>friendlyName</label>
                        <input type="text" class="form-control" v-model="irdata_update.friendlyName">
                    </div>
                    <div class="form-group">
                        <input type="checkbox" v-model="irdata_check.description">
                        <label>description</label>
                        <input type="text" class="form-control" v-model="irdata_update.description">
                    </div>
                    <div class="form-group">
                        <input type="checkbox" v-model="irdata_check.irdata_on">
                        <label>irdata_on</label>
                        <textarea class="form-control" rows="4" v-model="irdata_update.irdata_on"></textarea>
                        <button class="btn btn-default" v-on:click="irdata_learn_update_on()">赤外線受信(ON)</button>
                    </div>
                    <div class="form-group">
                        <input type="checkbox" v-model="irdata_check.irdata_off">
                        <label>irdata_off</label>
                        <textarea class="form-control" rows="4" v-model="irdata_update.irdata_off"></textarea>
                        <button class="btn btn-default" v-on:click="irdata_learn_update_off()">赤外線受信(OFF)</button> 
                    </div>
                    <button class="btn btn-primary" v-on:click="irdata_update_call(irdata_update.endpointId)">更新実行</button> 
                    <button class="btn btn-default" data-toggle="collapse" href="#irdata-update">閉じる</button>
                </div>
            </div>
        </div>

        <button class="btn btn-default" v-on:click="irdata_list_update()">テーブル更新</button>

        <table class="table table-striped">
            <thead>
                <tr><th>#</th><th>endpointId</th><th>friendlyName</th><th>irdata_on</th><th>irdata_off</th></tr>
            </thead>
            <tbody>
                <tr v-for="(irdata, index) in irdata_list">
                    <td><a v-on:click="irdata_show_detail(index)">{{index + 1}}</a></td>
                    <td><a v-on:click="irdata_show_detail(index)">{{irdata.endpointId}}</td></a>
                    <td>{{irdata.friendlyName}}</td>
                    <td><button class="btn btn-default" v-on:click="irdata_fire(irdata.endpointId, 'irdata_on')">赤外線送信(ON)</button></td>
                    <td><button class="btn btn-default" v-on:click="irdata_fire(irdata.endpointId, 'irdata_off')">赤外線送信(OFF)</button></td>
                </tr>
            </tbody>
        </table>

        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="js/start.js"></script>
</body>
```

js/start.js

```start.js
const base_url = '【RESTful呼び出しのルートURL】';

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',
        irdata_register: {},
        irdata_update: {},
        irdata_check: {},
        irdata_list: []
    },
    computed: {
    },
    methods: {
        irdata_fire: async function(endpointId, type){
            var param = {
                endpointId: endpointId,
                type: type,
            };
            return do_post(base_url + '/rm-fire', param )
            .then(result =>{
                console.log(result);
                alert('送信しました。');
            });
        },
        irdata_update_delete: async function(endpointId){
            if(!window.confirm('本当に削除しますか？'))
                return;

            var param = {
                endpointId: endpointId,
            };
            return do_post(base_url + '/rm-delete', param )
            .then(result =>{
                console.log(result);
                this.panel_close('#irdata-update');
                alert(result.endpointId + 'を削除しました。');
            });
        },
        irdata_update_call: async function(endpointId){
            var param = {
                endpointId: endpointId,
                friendlyName: (this.irdata_check.friendlyName) ? this.irdata_update.friendlyName : undefined,
                description: (this.irdata_check.description) ? this.irdata_update.description : undefined,
                irdata_on: (this.irdata_check.irdata_on) ? this.irdata_update.irdata_on : undefined,
                irdata_off: (this.irdata_check.irdata_off) ? this.irdata_update.irdata_off : undefined,
            };
            return do_post(base_url + '/rm-update', param )
            .then(result =>{
                console.log(result);
                this.panel_close('#irdata-update');
                alert(result.endpointId + 'を更新しました。');
            });
        },
        irdata_show_detail: function(index){
            this.irdata_update = JSON.parse(JSON.stringify(this.irdata_list[index]));
            this.panel_open('#irdata-update');
        },
        irdata_register_call: async function(){
            var param = {
                friendlyName : this.irdata_register.friendlyName,
                description: this.irdata_register.description,
                irdata_on: this.irdata_register.irdata_on,
                irdata_off: this.irdata_register.irdata_off
            };
            return do_post(base_url + '/rm-create', param )
            .then(result =>{
                console.log(result);
                this.panel_close('#irdata-register');
                alert(result.endpointId + 'に登録しました。');
            });
        },
        irdata_list_update: async function(){
            return do_post(base_url + '/rm-get-list', {})
            .then(result =>{
                console.log(result);
                this.irdata_list = result.list;
            });
        },
        irdata_learn_register_on: async function(){
            this.progress_open('リモコンを押して赤外線を送信してください。');
            return do_post(base_url + '/rm-learn', {} )
            .then(result =>{
                this.progress_close();
                console.log(result);
                if( result.irdata ){
                    var t = JSON.parse(JSON.stringify(this.irdata_register));
                    t.irdata_on = result.irdata;
                    this.irdata_register = t;
                }else{
                    alert('赤外線を受信できませんでした。');
                }
            });
        },
        irdata_learn_register_off: async function(){
            this.progress_open('リモコンを押して赤外線を送信してください。');
            return do_post(base_url + '/rm-learn', {} )
            .then(result =>{
                this.progress_close();
                console.log(result);
                if( result.irdata ){
                    var t = JSON.parse(JSON.stringify(this.irdata_register));
                    t.irdata_off = result.irdata;
                    this.irdata_register = t;
                }else{
                    alert('赤外線を受信できませんでした。');
                }
            });
        },
        irdata_learn_update_on: async function(){
            this.progress_open('リモコンを押して赤外線を送信してください。');
            return do_post(base_url + '/rm-learn', {} )
            .then(result =>{
                this.progress_close();
                console.log(result);
                if( result.irdata ){
                    var t = JSON.parse(JSON.stringify(this.irdata_update));
                    t.irdata_on = result.irdata;
                    this.irdata_update = t;
                }else{
                    alert('赤外線を受信できませんでした。');
                }
            });
        },
        irdata_learn_update_off: async function(){
            this.progress_open('リモコンを押して赤外線を送信してください。');
            return do_post(base_url + '/rm-learn', {} )
            .then(result =>{
                this.progress_close();
                console.log(result);
                if( result.irdata ){
                    var t = JSON.parse(JSON.stringify(this.irdata_update));
                    t.irdata_off = result.irdata;
                    this.irdata_update = t;
                }else{
                    alert('赤外線を受信できませんでした。');
                }
            });
        },
        dialog_open: function(target){
            $(target).modal({backdrop:'static', keyboard: false});
        },
        dialog_close: function(target){
            $(target).modal('hide');
        },
        panel_open: function(target){
            $(target).collapse("show");
        },
        panel_close: function(target){
            $(target).collapse("hide");
        },
        progress_open(title = '少々お待ちください。'){
            this.progress_title = title;
            this.dialog_open('#progress');
        },
        progress_close(){
            this.dialog_close('#progress');
        },
    },
    created: function(){
    },
    mounted: function(){
    }
};
var vue = new Vue( vue_options );

function do_post(url, body){
    const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } );
    
    return fetch(url, {
        method : 'POST',
        body : JSON.stringify(body),
        headers: headers
    })
    .then((response) => {
        return response.json();
    });
}
```

index.htmlを開くと以下のようなページが表示されます。
テーブル更新ボタンを押下すると、保存しておいた赤外線データのリストが表示されます。最初は何も登録されていないかと思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b528f672-2893-a416-74c5-5b7e857097b0.png)

登録をクリックすると、赤外線データの入力フォームが表示されます。
friendlyNameやdescriptionにはわかりやすいように文字列を入力します。（実はこのfriendlyNameがスマートホームでのデバイス名として見えるようになります。）
赤外線受信ボタンを押下することで、赤外線学習待ちになり、赤外線を黒豆に対して向けると学習してその下のテキストエリアに表示されます。
irdata_onとirdata_offの2つがありますが、Alexaスマートホームでは1つのデバイスにOnとOffの2つの制御ができることを想定していますので、それに合わせて赤外線も1デバイスに対して2つを記憶できるようにしました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/3b59eab8-560d-4205-4d9f-c438d764e3fb.png)

更新も同様ですが、更新したい項目にあるチェックボックスをOnにしてから更新実行を押下することで、保持している赤外線データのうちのOnにした項目だけ更新されるようにしています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/a2fc1339-adb9-1302-abcc-730387d8611f.png)

記憶した赤外線受信の発信を試したい場合は、テーブル更新ボタンを押下したときに表示されたテーブルのところの「赤外線送信(ON)」ボタンや「赤外線送信(OFF)」ボタンを押下することでできます。

とりあえず、黒豆を使って赤外線の学習と発信ができる環境ができました。

以上です。