yamamoWorks

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

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

前回までにWehookを起点にPS4を制御するところまで実装できました。今回はいよいよGoogle Home(Assistant)との連携です。

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後編】
Google HomeとAzure IoT HubとRaspberry PiでTVとPS4をコントロール【Functions編】

IFTTT

IFTTT(If This Then That)はWebサービス同士を連携させるプラットフォームで、Google Home(Assistant)やAmazon Echo(Alexa)なども対応しており、スマートスピーカーにカスタム処理を実装する際の1つの方法として利用できます。

Sign upしたらMy Appletsから「New Applet」ボタンをクリックします。 次に[+]thisをクリックしGoogle Assistantを探して選択します。

f:id:yamamoWorks:20180407225314p:plain:w300 f:id:yamamoWorks:20180407225153p:plain:w300

Google Assistantのtriggerは4つ用意されています。

  • Say a simple phrase
  • Say a phrase with a number
  • Say a phrase with a text ingredient
  • Say a phrase with both a number and a text ingredient

今回実現したいのは「torneを起動して」や「Netflixを起動して」なので Say a phrase with a text ingredient を使用して「○○を起動して」という1つのアプレットを定義したいのですが、可変となるtextを先頭にする事が出来ない制約がある為、以下のどちらかで実現する必要があります。

  1. Say a simple phrase を使用して起動したいアプリ毎にアプレットを作成する。
  2. Say a phrase with a text ingredient を使用して「プレステで○○を起動して」という1つのアプレットを作成する。

今回は発声を短くしたいので前者を採用します。

「What do you whant to say?」に認識して欲しい音声を入力し「What do you want the Assistant to say in response?」にGoogle Homeが発声する応答を入力します。「What's another way to say it?」と「And another way?」はオプショで別の言い方を登録できます。
f:id:yamamoWorks:20180505232830p:plain

「Create trigger」ボタンを押して保存したら次に[+]thatをクリックしWebhooksを探して選択します。

ここで【Functions編】で作成したFunctionsのURLを取得しておきます。
f:id:yamamoWorks:20180505233437p:plain

URLに上記で取得したURLを入力し、MethodをPOST、Content Typeをapplication/json、Bodyには【Functions編】のテストで使用した内容を入力して完成です!
f:id:yamamoWorks:20180505231526p:plain
同様に他の起動したいアプリのアプレットも作成します。代表的なアプリのTitleIdを載せておきます。
ちなみに「torne」はGoogle Homeが音声認識してくれないので「トルネ」で登録が必要です。 また「Amazon Video」だとChromecastデバイスを探そうとするので「プライムビデオ」で登録しないとダメでした。

Title Id
Netflix CUSA02988
torne CUSA00442
Spotify CUSA01780
Amazon Prime Video CUSA03099
Monster Hunter World CUSA06027

f:id:yamamoWorks:20180506001618p:plain

後は【PS4後編】で作成したプログラムを実行した状態で「OK Google, Netflixを起動して」と言えばPS4がスタンバイモードから起動してNetflixが起動します!

動作確認まで出来たので、次回はRaspberry Piにプログラムを載せます。

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から声で操作できるようにします。

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

前回はps4-wakerをセットアップしコマンド実行でPS4を制御しました。今回はプログラムからps4-wakerのライブラリを呼び出す方法の紹介と、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前編】

ps4-waker

ps4-wakeを使ってNode.jsからPS4を制御するコードは簡単です。スタンバイモードから復帰してNetflixを起動する場合は下記のようになります。ps4-wakerのメソッドはPromiseパターン(?)で実装されているのでコールバック地獄にならずにスラッと記述できます。

const Device = require("ps4-waker").Device;
var ps4 = new Device();
ps4.turnOn()
   .then(() => startTitle('CUSA02988'))
   .then(() => ps4.close());

SmartHome Client

IoT Hubに接続してダイレクトメソッド呼び出しを受けたらPS4を制御するコードを実装します。ps4-wakerがPromiseパターンなのでIoT Hub ClientもBluebirdを使ってPromise化しています。またIoT Hubの接続文字列は環境変数「IOTHUB_CONNECTION_STRING」から取得するようにしています。
なおNode.js初心者なのでベストな実装かどうかは分かりません(´・ω・`)

"use strict";

const Bluebird = require("bluebird");
const Mqtt = require("azure-iot-device-mqtt").Mqtt;
const Client = require("azure-iot-device").Client;
const Device = require("ps4-waker").Device;

const connectionString = process.env.IOTHUB_CONNECTION_STRING;
const client = Bluebird.promisifyAll(Client.fromConnectionString(connectionString, Mqtt));


const onSendPS4Command = (request, response) => {
    console.log(request.payload);
    let command = request.payload.command;
    let titleId = request.payload.titleId;

    var res = Bluebird.promisifyAll(response);
    let ps4 = new Device({
        bindAddress: process.argv[2]
    });

    Promise.resolve()
        .then(() => command == "standby" ? ps4.turnOff() : ps4.turnOn())
        .then(() => command == "start" ? ps4.startTitle(titleId) : null)
        .then(() => ps4.close())
        .then(() => res.sendAsync(200, "success"))
        .catch(err => {
            console.error(err.toString());
            res.sendAsync(500, err.toString());
        });
};

client.openAsync()
    .then(() => {
        console.log("iot-hub client opened.");
        client.onDeviceMethod("sendPS4Command", onSendPS4Command);
    })
    .catch(err => console.error(`could not open iot-hub client. ${err.toString()}`));

次回はGoogle Home(Assistant)からIoT Hub経由でダイレクトメソッドを呼び出す部分を実装します。

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

前回までに全体像とAzure IoT Hubの設定まで行いました。今回はPlayStation 4の制御とIoT Hubとの連携を実装します。最終的にはRaspberry Piで実行しますが先ずはWindows上のNode.jsで実装してみます。

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

PS4の制御

PS4の制御には「ps4-waker」というライブラリを使います。このライブラリでは同一LAN上のPS4に対して以下の事が可能です。

  • 起動(スタンバイモードから)
  • スタンバイ
  • アプリの起動
  • キー送信(up, down, left, right, enter, back, option, ps)
  • テキスト送信

ps4-wakerの導入

プログラムを作る前に、PS4にアクセスするにはPINコードでの認証が必要なので以下の手順で先に済ませておきます。なお作業にはPS4 Second ScreenというiPhone/Androidアプリが必要となります。 (PS4 Second ScreenはPlayStation Appのセカンドスクリーン機能が別アプリとして切り出されたものです)

PSN | PlayStation App | プレイステーション
※PS4 Second Screenは「PlayStation関連アプリ」にあります

まずPowerShellかコマンドプロンプトからps4-wakerをインストールします。

PS > npm install -g ps4-waker

インストールが完了したらコマンドを実行します。すると認証情報が無いのでPS4 Second Screen Appで接続しろと表示されます。

PS > npx ps4-waker
No credentials; Use the PS4 Second Screen App and try to connect to PS4-Waker

実行している端末にネットワークインターフェイスが複数ある場合は、PS4と同一LAN上のインターフェイス(IPアドレス)を--bind 192.168.0.1のように指定する必要があります。

この状態のまま、同一LAN上にあるスマホでPS4 Second Screenを起動すると「PS4-Waker」が見つかるので選択します。
f:id:yamamoWorks:20180318233559p:plain:w300

するとps4-wakerがPINコード入力待ち状態になります。 この時点でPS4 Second Screenは終了しても構いません。

Got credentials!  { 'client-type': 'i',
  'auth-type': 'C',
  'user-credential': 'xxxxxxxxxxxxxxxxxxxxxxxx' }
Go to 'Settings -> PlayStation(R) App Connection Settings -> Add Device' on your PS4 to obtain the PIN code.
Pin code> 

次にPS4側で[設定] - [モバイルアプリ接続設定] - [機器を登録する]と進み、表示されたPINコードをps4-wakerに入力します。
f:id:yamamoWorks:20180318164306p:plain:w300 f:id:yamamoWorks:20180318164323p:plain:w300

Pin code> 00000000
Logged into device! Future uses should succeed

以上でps4-wakerを実行している端末からPS4の制御が可能となりました。

認証情報はHOMEディレクトリに.ps4-wake.credentials.jsonファイルとして保存されます。Linux系/macOSなら~/ですが、Windowsだと実行時のカレントドライブ+%HOMEPATH%のようです。

動作確認

試しにコマンドを送ってみましょう。

PS > npx ps4-waker standby

PS4がスタンバイモードになれば成功です。

アプリを起動する場合は以下のようなコマンドになります。スタンバイモードになっていても自動的にPS4が起動します。

PS > npx ps4-waker start CUSA02988

ここで「CUSA02988」はNetflixアプリを表すTitle Idと呼ばれるコードです。
※Title Idは同じアプリでも国・地域によって値が異なります。

Application Title Id (JP) Title Id (US) Title Id (EU)
Netflix CUSA02988 CUSA00129 CUSA00127
YouTube CUSA01065 CUSA01015 CUSA01116
Amazon Video CUSA03099 CUSA00130 CUSA00126

Title Idを調べるには、PS4で目的のアプリを起動した状態でsearchコマンドを送信、表示された応答のrunning-app-titleidを確認します。

{
  "type": "device",
  "statusLine": "200 Ok",
        (省略)
  "running-app-name": "Spotify",
  "running-app-titleid": "CUSA01780",
  "address": "192.168.0.2"
}

次回はPS4にコマンドを送信するプログラムを作成します。

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

前回は「概要編」ということで、これから作る全体像を示しました。今回からは各パーツの設定や作成を行っていきます。

IoT Hub作成

最初は同じくAzureのEvent Hubで構想していたのですが、Freeプランがありデバイスにダイレクトメソッド呼び出しができるIoT Hubで作ることにします。

Azure Portalより、リソースの作成「+」ボタンで「IoT Hub」を探して「作成」
f:id:yamamoWorks:20180305221832p:plain

「名前」は公開されるURLの一部(xxxxx.azure-devices.net)になるので既に使われている名前はつけられません。 「価格とスケールティア」は 今回の用途では1日に8000メッセージを処理できる「F1 Free」で十分ですね。 後は「サブスクリプション」と「リソースグループ」を選び「作成」ボタンを押します。
f:id:yamamoWorks:20180305221828p:plain:w300

デバイス登録

次にRaspberry Piをデバイスとして登録します。 作成したIoT Hubを開き「デバイスエクスプローラー」の「+追加」ボタンを押し、任意のデバイスIDを入力して「保存」ボタンを押します。
f:id:yamamoWorks:20180306000805p:plain:w400

接続テスト

公式ドキュメントを参考に、クラウドから呼び出すことができるダイレクトメソッドを持つデバイス側のプログラムと、IoT Hub を介してデバイス側のダイレクトメソッドを呼び出すプログラムをNode.jsで作成しWindows上で実行します。 「シミュレーション対象デバイス アプリの作成」以下を手順通りに実施するだけなので詳細は省きますが、指定する接続文字列について触れておきます。

SimulatedDevice.js で指定する {device connection string} は先程ポータルで作成したデバイスの詳細にある文字列です。
f:id:yamamoWorks:20180312232222p:plain:w400

CallMethodOnDevice.js で指定する {iothub connection string} は「共有アクセスポリシー」-「service」の詳細にある接続文字列です。
f:id:yamamoWorks:20180312232605p:plain:w400

IoT Hubを介してデバイス側のメソッドを呼び出す事が出来ました。
f:id:yamamoWorks:20180312234546g:plain

次回はPS4の制御方法を解説します。