yamamoWorks

.NET技術を中心に気まぐれに更新していきます

Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【05:Functions編】

前回までにAzure IoT Hub経由で呼ばれたらPS4を制御する部分は実装できました。今回から呼び出す側の実装です。

Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【概要編】
Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【IoT Hub編】
Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【PS4前編】
Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【PS4後編】

Azure IoT HubはREST APIが用意されているのでIFTTTからWebhookで直接叩きたいところですが、IFTTTのWebhookではHTTPヘッダーの設定が出来ず認証情報を渡せないのと、認証情報は都度計算が必要なShared Access Signaturesである事から、Azure Functionsでいい感じに処理できるようにします。

処理概要

IFTTTから共通アクセスキー等の情報を要求本文で受け取りShared Access Signaturesに変換してIoT Hub REST APIに送信する。 f:id:yamamoWorks:20180504060222p:plain

Functionsの作成

Azure Portalより、リソースの作成「+」ボタンで「Functions」を探して「作成」
f:id:yamamoWorks:20180504061609p:plain あとは必要な情報を入力。
f:id:yamamoWorks:20180504061726p:plain:w400

作成されたFunction Appの「関数」から「新しい機能」を押し「HTTP trigger」を選択。
f:id:yamamoWorks:20180504063009p:plain:w300 f:id:yamamoWorks:20180504065655p:plain:w300

今回は言語をJavaScriptにしてサーバ側もNodejsで実装します。
f:id:yamamoWorks:20180504064244p:plain:w300

作成されたFunctionの「統合」でモードを「Webhook」、webhookの種類を「Generic JSON」に設定。 またソースコードが分かりやすいように要求パラメータ名を「req」から「fncReq」に変更しておきます。
f:id:yamamoWorks:20180504065342p:plain:w500

Functionsの実装

IoT REST APIの認証情報はAuthorizationヘッダーにShared Access Signaturesが必要です。これは共有アクセスキーやエンドポイントから計算するもので、ソースコードが提示されているのでそのまま使います。

Node.jsでHTTPリクエストを行なう際は通常はrequestモジュールを利用するかと思いますが、Functionsで外部モジュールを使うようにする説明が面倒臭いので 今回は標準のHTTPSモジュールで実装しています。

var https = require("https");
var url = require('url');
var crypto = require("crypto");

module.exports = function (context, fncReq) {
    var urlInfo = url.parse(fncReq.endpoint, true);
    var sasToken = generateSasToken(urlInfo.host + urlInfo.path, fncReq.sharedAccessKey, fncReq.policyName, 5);
    var data = JSON.stringify(fncReq.data);

    var options = {
        host: urlInfo.host,
        method: "POST",
        path: urlInfo.path,
        headers: {
            "Content-Type": "application/json",
            "Authorization": sasToken,
            "Content-Length": Buffer.byteLength(data)
        }
    };

    var hubReq = https.request(options, hubRes => {
        var body = "";
        hubRes.setEncoding("utf-8");
        hubRes.on("data", chunk => body = body + chunk);
        hubRes.on("end", () => {
            context.res = {
                status: hubRes.statusCode,
                body: body
            };
            context.done();
        })
    });

    hubReq.on("error", e => {
        context.res = {
            status: 500,
            body: e.message
        };
        context.done();
    });

    hubReq.write(data);
    hubReq.end();
};

// https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-devguide-security#security-tokens
var generateSasToken = function(resourceUri, signingKey, policyName, expiresInMins) {
    resourceUri = encodeURIComponent(resourceUri);

    // Set expiration in seconds
    var expires = (Date.now() / 1000) + expiresInMins * 60;
    expires = Math.ceil(expires);
    var toSign = resourceUri + '\n' + expires;

    // Use crypto
    var hmac = crypto.createHmac('sha256', new Buffer(signingKey, 'base64'));
    hmac.update(toSign);
    var base64UriEncoded = encodeURIComponent(hmac.digest('base64'));

    // Construct autorization string
    var token = "SharedAccessSignature sr=" + resourceUri + "&sig="
    + base64UriEncoded + "&se=" + expires;
    if (policyName) token += "&skn="+policyName;
    return token;
};

Functionsのテスト

Azure PortalでFunctionsを実行する機能があるので、前回作成したSmartHome Clientを起動した状態で実行してみます。要求本文は以下の通り。

{
  "endpoint": "https://{IoT Hub名}.azure-devices.net/twins/raspi/methods?api-version=2016-11-14",
  "policyName": "service",
  "sharedAccessKey": "{IoT Hubの共有アクセスポリシー「service」の共有アクセスキー}",
  "data": {
    "methodName": "sendPS4Command",
    "payload": {
      "command": "start",
      "titleId": "CUSA02988"  <-- Netflix
    }
  }
}

f:id:yamamoWorks:20180504072502p:plain

次回はいよいよIFTTTを設定してGoogle Homeから声で操作できるようにします。