N@i.jp  昨日:4670
 今日:0189
 総計:00209668
keywords
管理者専用
  Post   Add link   Control Panel 






























新しいトピック
最新:12/03 11:42


新しいコメント
最新:06/04 19:41






管理人へMAIL

IRKitスマートホームスキルの改良(番外編)

callback地獄を回避するため、少しモダンな書き方をしてみました


 今はIRKitのスキルから離れて、全然別のスキルを作っています。アイディアとしては Echoを何か連絡手段として使えないかと色々試しています。やっと少し動き始めたという感じで、まだ実用には遠いですが、そのうち何か記事を書けるようになれたらいいなぁ・・・


 Node.js(Java Script)は非同期I/Oなので結果を得るためにcallback関数を使うのですが、逐次的な処理を行おうとするとプログラムの入れ子が深くなり、構造が非常に分かりにくくなります。いわゆる callback地獄というもので、これを回避するために、モダンなNode.js(Java Script)では Promise, Deffered という概念が導入されているそうです。
 と言うことで、IRKitスマートホームスキルにも赤外線信号の送信、DynamoDBのアクセスという非同期処理がありますので、これを Promise, Deferred を使ったモダンなスタイルに書き換えてみました。あと、callbackの無名関数を定義するときには "function" ではなく "=>" を使うようにもしてみました。これで使い方あってる?

'use strict';
var AWS = require('aws-sdk');   // AWS SDKを使用
AWS.config.update({region: "us-west-2"});

// IRKitデバイス情報
const IRKit_DEVICE = process.env.My_IRKit_NAME;
const clientkey = process.env.My_ClientKey;
const deviceid = process.env.My_DeviceID;
const IRKit_Light_ID = IRKit_DEVICE + '-light-' + deviceid; // 照明デバイスの識別子

// エラー情報
const sendError = Error("Send error");          // 赤外線信号の送信が失敗
const internalError = Error("Internal error");  // 内部エラー
// エラーメッセージ
const InvDeviceMsg = "そのようなデバイスは登録されていません。";
const InvValueMsg = "指定範囲外の値のため、設定できません。";
const UnSupportMsg = "その操作には対応していません。";

// リモコン信号
const LightIR = {
  on: { "format":"raw",
        "freq":38,
        "data":[18031,8755,1190,1037,1190,3228,1190,1037,1190,1037,1190,1037,
                1190,1037,1190,1037,1190,3228,1190,3228,1190,1037,1190,3228,
                1190,3228,1190,1037,1190,3228,1190,3228,1190,1037,1190,3228,
                1190,3228,1190,3228,1190,3228,1190,3228,1190,1037,1190,1037,
                1190,1037,1190,1037,1190,1037,1190,1037,1190,1037,1190,1037,
                1190,3228,1190,3228,1190,3228,1190,65535,0,18031,18031,4400,
                1190,65535,0,65535,0,60108,17421,4400,1190]
  },
  off: {"format":"raw",
        "freq":38,
        "data":[18031,8755,1190,1037,1190,3228,1190,1037,1190,1037,1190,1037,
                1190,1037,1190,1037,1190,3228,1190,3228,1190,1037,1190,3228,
                1190,3228,1190,1037,1190,3228,1190,3228,1190,1037,1190,1037,
                1190,3228,1190,3228,1190,3228,1190,3228,1190,1037,1190,1037,
                1190,1037,1190,3228,1190,1037,1190,1037,1190,1037,1190,1037,
                1190,3228,1190,3228,1190,3228,1190,65535,0,18031,18031,4400,
                1190,65535,0,65535,0,60108,17421,4400,1190]
  },
  dim: {"format":"raw",
        "freq":38,
        "data":[18031,8755,1190,1037,1190,3228,1190,1037,1190,1037,1190,1037,
                1190,1037,1190,1037,1190,3228,1190,3228,1190,1037,1190,3228,
                1190,3228,1190,1037,1190,3228,1190,3228,1190,1037,1190,1037,
                1190,3228,1190,3228,1190,1037,1190,3228,1190,1037,1190,1037,
                1190,1037,1190,3228,1190,1037,1150,1037,1150,3228,1190,1037,
                1150,3228,1190,3228,1190,3228,1190,65535,0,18031,18031,4400,
                1190,65535,0,65535,0,60108,17421,4400,1190]
  }
};

// リモコン信号の送信
function sendIR(ir) {
    var deferred = Promise.defer(); // Deferredオブジェクトを作る
    var https = require("https");
    var qs = require("querystring");
    var postData = qs.stringify({
        clientkey: clientkey,
        deviceid: deviceid,
        message: JSON.stringify(ir)
    });
    var req = https.request({
        host: "api.getirkit.com",
        path: "/1/messages",
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
            "Content-Length": postData.length
        }
    }, (res) => {
        var result = res.statusCode;
        if (result == 200) {
            console.log("res.statusCode=" + result);    // ログ出力
            deferred.resolve(null, "success");          // 送信成功
        }
        else {
            console.log(postData);                      // ログ出力
            console.log("res.statusCode=" + result);    // ログ出力
            deferred.reject(sendError);                 // 送信エラーを通知
        }
    });
    req.write(postData);
    req.end();
    return deferred.promise;    // Promiseを返す
}

exports.handler = function (request, context, callback) {
    var header = request.directive.header;
    var req_name = header.name;

    switch (header.namespace) {
        case 'Alexa.Discovery':
            if (req_name === 'Discover') {
                log("DEGUG: ", "Discover request", JSON.stringify(request));
                handleDiscovery(request, context, callback);
            }
            break;

        case 'Alexa.PowerController':
            if (req_name === 'TurnOn' || req_name === 'TurnOff') {
                 log("DEBUG: ", "TurnOn or TurnOff Request", JSON.stringify(request));
                 handlePowerControl(request, context, callback);
            }
            break;

        case 'Alexa.BrightnessController':
            if (req_name === 'AdjustBrightness' || req_name === 'SetBrightness') {
                log("DEBUG: ", "Brightness Request", JSON.stringify(request));
                handleBrightnessControl(request, context, callback);
            }
            break;

        case 'Alexa':
            if (req_name === 'ReportState') {
                log("DEBUG: ", "Report State", JSON.stringify(request));
                handleState(request, callback);
            }
            break;

        default: {
            // サポートしていない操作
            log("DEBUG: ", "Unsupported request", JSON.stringify(request));
            callback(null, generateNotSupportResponse(request));
        }
    }

    // 照明デバイスの検出
    function handleDiscovery(request, context, callback) {
        var payload = {
            "endpoints":
            [
                {
                    "endpointId": IRKit_Light_ID,
                    "manufacturerName": "IRKit",
                    "friendlyName": "リビングの照明",	// このデバイス名で検出されます
                    "description": "リビングの照明をIRKitで操作できます。",
                    "displayCategories": ["LIGHT"],		// 照明器具
                    "cookie": {},
                    "capabilities":
                    [
                        {
                            "type": "AlexaInterface",
                            "interface": "Alexa",
                            "version": "3"
                        },
                        {
                            "type": "AlexaInterface",
                            "interface": "Alexa.PowerController",
                            "version": "3",
                            "properties": {
                                "supported": [{
                                    "name": "powerState"	// 電源のオン・オフ
                                }],
                                "retrievable": true
                            }
                        },
                        {
                            "type": "AlexaInterface",
                            "interface": "Alexa.BrightnessController",
                            "version": "3",
                            "properties": {
                                "supported": [{
                                    "name": "brightness"    // 輝度
                                }],
                                "retrievable": true
                            }
                        },
                        {
                            "type": "AlexaInterface",
                            "interface": "Alexa.EndpointHealth",
                            "version": "3",
                            "properties":{
                                "supported":[{
                                    "name":"connectivity"   // 動作状態
                                }],
                                "retrievable": true
                            }
                        }
                    ]
                }
            ]
        };
        var header = request.directive.header;
        header.name = "Discover.Response";
        log("DEBUG: ", "handleDiscovery", JSON.stringify({ header: header, payload: payload }));
        callback(null, { event: { header: header, payload: payload } });
    }

    // 照明の電源操作
    function handlePowerControl(request, context, callback) {
        var requestMethod = request.directive.header.name;
        var endpoint = request.directive.endpoint;
        var deviceId = endpoint.endpointId;         // 電源操作対象のデバイス識別子
        var powerState, brightness;
        var ir;
        var response;

        if (requestMethod === "TurnOn") {
            log("DEBUG: ", "handlePowerControl: ", "Light on");
            powerState = "ON";
            brightness = 100;
            ir = LightIR.on;
        }
        else if (requestMethod === "TurnOff") {
            log("DEBUG: ", "handlePowerControl: ", "Light off");
            powerState = "OFF";
            brightness = 0;
            ir = LightIR.off;
        }
        if (deviceId != IRKit_Light_ID) {
            // 無効なデバイス
            response = generateInvDevResponse(request);
            log("DEBUG: ", "Invalid deviceId:", JSON.stringify(response));
            callback(null, response);
        }
        response = generatePowerResponse(request, powerState);
        sendIR(ir)
        .then(writeDynamoDBItem(powerState, brightness))    // DBに書き込む
        .then(() => {
            //log("DEBUG: ", "handlePowerControl", JSON.stringify(response));
            callback(null, response);
        })
        .catch((error) => {
            callback(error);    // 送信、DB書き込みが失敗
        });
    }

    // 照明の輝度操作
    function handleBrightnessControl(request, context, callback) {
        var requestMethod = request.directive.header.name;
        var endpoint = request.directive.endpoint;
        var payload  = request.directive.payload;
        var deviceId = endpoint.endpointId;             // 輝度操作対象のデバイス識別子
        var ir = LightIR.dim;
        var response;

        readDynamoDBItem()
        .then((data) => {
            var powerState = "ON";
            var brightnessResult = data.Item.brightness;
            if (requestMethod === "SetBrightness") {
                log("DEBUG: ", "handleBrightnessControl: ", "Set Brightness");
                let val = parseInt(payload.brightness, 10);
                if (val > 100 || val < 0) {
                    callback(null,generateInvValueResponse(request));
                }
                brightnessResult = val;
            }
            else if (requestMethod === "AdjustBrightness") {
                log("DEBUG: ", "handleBrightnessControl: ", "Adjust Brightness");
                let val = parseInt(payload.brightnessDelta, 10);
                if (val > 100 || val < -100) {
                    callback(null,generateInvValueResponse(request));
                }
                brightnessResult += val;
            }
            if (brightnessResult >= 100) {
                brightnessResult = 100;
                ir = LightIR.on;
            }
            else if (brightnessResult <= 0) {
                powerState = "OFF";
                brightnessResult = 0;
                ir = LightIR.off;
            }
            if (deviceId == IRKit_Light_ID) {
                response = generateBrightnessResponse(request, brightnessResult);
            }
            else {
                // 無効なデバイス
                response = generateInvDevResponse(request);
                log("DEBUG: ", "Invalid deviceId", JSON.stringify(response));
                callback(null, response);
            }
            sendIR(ir)
            .then(writeDynamoDBItem(powerState, brightnessResult))  // DBに書き込む
            .then(() => {
                //log("DEBUG: ", "handleBrightnessControl", JSON.stringify(response));
                callback(null, response);
            })
            .catch((error) => {
                callback(error);    // 送信、DB書き込みが失敗
            });
        })
        .catch((error) => {
            // DynamoDBからの読み出しが失敗
            callback(error);
        });
    }

    // 照明の状態(DynamoDBに保存されている値で代替)レスポンスを返却する
    function handleState(request, callback) {
        readDynamoDBItem()
        .then((data) => {
            let res = generateStateResponse(request,
                                            data.Item.powerState,
                                            data.Item.brightness);
            //log("DEBUG: ", "handleState", JSON.stringify(res));
            callback(null, res);
        })
        .catch((error) => {
            // DynamoDBからの読み出しが失敗
            callback(error);
        });
    }
};

// 存在していないエンドポイント(デバイス)へのディレクティブ
function generateInvDevResponse(request) {
    return generateErrorResponse(request, "NO_SUCH_ENDPOINT", InvDeviceMsg);
}

// 無効な値を設定しようとするディレクティブ
function generateInvValueResponse(request) {
    return generateErrorResponse(request, "INVALID_VALUE", InvValueMsg);
}

// サポートしていないディレクティブ
function generateNotSupportResponse(request) {
    return generateErrorResponse(request, "INVALID_DIRECTIVE", UnSupportMsg);
}

// エラー応答(JSON)を生成する
function generateErrorResponse(request, error_type, message) {
    var responseHeader = request.directive.header;
    var endpoint = request.directive.endpoint;
    var deviceId = endpoint.endpointId;         // デバイス識別子
    var requestToken = endpoint.scope.token;

    responseHeader.namespace = "Alexa";
    responseHeader.name = "ErrorResponse";
    responseHeader.messageId = responseHeader.messageId + "-R";
    return {
        event: {
            header: responseHeader,
            endpoint: {
                "scope":{
                    "type": "BearerToken",
                    "token": requestToken
                },
                "endpointId": deviceId,
            },
            payload: {
                "type": error_type,
                "message": message
            }
        }
    };
}

// 電源操作の応答(JSON)を生成する
function generatePowerResponse(request, powerResult) {
    var now = new Date();
    var contextResult = {
        "properties": [{
            "namespace": "Alexa.PowerController",
            "name": "powerState",
            "value": powerResult,
            "timeOfSample": now,
            "uncertaintyInMilliseconds": 0
        }]
    };
    return generateResonse(request, "Response", contextResult);
}

// 輝度調整の応答(JSON)を生成する
function generateBrightnessResponse(request, brightnessResult) {
    var now = new Date();
    var contextResult = {
        "properties": [{
            "namespace": "Alexa.BrightnessController",
            "name": "brightness",
            "value": brightnessResult,
            "timeOfSample": now,
            "uncertaintyInMilliseconds": 0
        }]
    };
    return generateResonse(request, "Response", contextResult);
}

// 照明の状態の応答(JSON)を生成する
function generateStateResponse(request, powerState, brightness) {
    var now = new Date();
    var contextResult = {
        "properties": [
            {
                "namespace": "Alexa.PowerController",
                "name": "powerState",
                "value": powerState,
                "timeOfSample": now,
                "uncertaintyInMilliseconds": 0
            },
            {
                "namespace": "Alexa.BrightnessController",
                "name": "brightness",
                "value": brightness,
                "timeOfSample": now,
                "uncertaintyInMilliseconds": 0
            },
            {
                "namespace": "Alexa.EndpointHealth",
                "name": "connectivity",
                "value": {"value": "OK"},
                "timeOfSample": now,
                "uncertaintyInMilliseconds": 0
            }
        ]
    };
    return generateResonse(request, "StateReport", contextResult);
}

function generateResonse(request, responseName, contextResult) {
    var responseHeader = request.directive.header;
    var endpoint = request.directive.endpoint;
    var deviceId = endpoint.endpointId;         // デバイス識別子

    responseHeader.namespace = "Alexa";
    responseHeader.name = responseName;
    responseHeader.messageId = responseHeader.messageId + "-R";
    return {
        context: contextResult,
        event: {
            header: responseHeader,
            endpoint: {
                "endpointId": deviceId,
            },
            payload: {}
        }
    };
}

// DynamoDBのSmartIRKitテーブルの、id:'LivingLight'項目を読み出す
function readDynamoDBItem() {
    var docClient = new AWS.DynamoDB.DocumentClient();
    const params = {
        TableName: 'SmartIRKit',    // DynamoDBテーブル名
        Key:{ 'id': 'LivingLight' } // Primary Key
    };
    var deferred = Promise.defer(); // Deferredオブジェクトを作る

    docClient.get(params, (err, data) => {
        if (err) {
            log("DEBUG: ", "Unable to read item. Error JSON:", JSON.stringify(err, null, 2));
            deferred.reject(null);
        }
        log("DEBUG: ", "GetItem succeeded:", JSON.stringify(data));
        deferred.resolve(data);
    });
    return deferred.promise;        // Promiseを返す
}

// DynamoDBのSmartIRKitテーブルの、id:'LivingLight'項目を書き込む
function writeDynamoDBItem(powerState, brightness) {
    var docClient = new AWS.DynamoDB.DocumentClient();
    var writeData = {
        TableName: 'SmartIRKit',        // DynamoDBテーブル名
        Item:{
            'id': 'LivingLight',
            'powerState': powerState,
            'brightness': brightness
        }
    };
    var deferred = Promise.defer(); // Deferredオブジェクトを作る

    docClient.put(writeData, (err) => {
        if (err) {
            log("DEBUG: ", "Unable to write item. Error JSON:", JSON.stringify(err, null, 2));
            deferred.reject(err);
        }
        log("DEBUG: ", "PutItem succeeded:", JSON.stringify(writeData));
        deferred.resolve(null);
    });
    return deferred.promise;        // Promiseを返す
}

function log(message, message1, message2) {
    console.log(message + message1 + message2);
}


< 過去の記事 [ 12月の 全てのカテゴリ リスト ] 新しい記事 >

2018 calendar
12月
1
2345678
9101112131415
16171819202122
23242526272829
3031


掲示板
最新:08/15 17:19


GsBlog was developed by GUSTAV, Copyright(C) 2003, Web Application Factory All Rights Reserved.