yamamoWorks

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

Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【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から声で操作できるようにします。