前回の「Nest MiniでLocal Home SDKを使ってお家のIoTロボットを操作する 第2回」では、コードラボの「Smart Home Local Execution」をベースにクラウド経由での実行まで確認しました。
今回は、ローカル実行をできるようにしてみようと思います。
クラウドフルフィルメントを更新する
ローカル実行をサポートするには、otherDeviceIdsフィールドを追加する必要があります。「smarthome-local/app-start/functions」ディレクトリの 「index.js」に追加します。
「app.onSync」メソッドの 「// TODO: Add otherDeviceIds for local execution」の箇所に「otherDeviceIds」を追記します。
app.onSync((body) => {
return {
...
// TODO: Add otherDeviceIds for local execution
otherDeviceIds: [{
deviceId: 'deviceid123',
}],
...
};
});
下記コマンドを実行してデプロイします。
firebase deploy --only functions
Web UIを開いていたら、「Smart Home Codelab」の右側の更新アイコンを押下してください。

ローカル実行を構成する
ローカル実行アプリをFirebase Hostingに公開し、Google Homeデバイス(Nest Miniなど)がアクセスしてダウンロードできるようにします。
Actionsコンソールにアクセスし、「piccorobo-iot」プロジェクトを選択し、 「Test > On device testing」を選択します。
「Development server URL」に、「https://<project-id>.firebaseapp.com/local-home/index.html」を入力します。
これは、以降でローカルホームアプリを公開するURLになります。
「<project-id>」は自分のプロジェクトIDに置き換えてください。
入力したら、上部の「Save」ボタンを押下してください。

次にGoogle Homeデバイス(Nest Mini)が、ローカルスマートデバイス(ピッコロボIoT)を検出する方法を定義します。
「Develop > Actions」を選択します。

「Configure local home SDK」に下記項目を設定していきます。
| フィールド | 説明 | 設定値 |
|---|---|---|
| UDP discovery address | UDPブロードキャストアドレス | 255.255.255.255 |
| UDP discovery port out | Google HomeデバイスがUDPブロードキャストを送信するポート | 3311 |
| UDP discovery port in | Google Homeデバイスが応答のためにリッスンするポート | 3312 |
| UDP discovery packet | UDPブロードキャストデータペイロード。(ここでは「HelloLocalHomeSDK」という文字列の16進エンコードデータとしている) | 48656c6c6f4c6f63616c486f6d6553444b |
フィールドは、ドロップダウンボックスの一覧から選択することができます。

全部入力すると、このようになります。
入力が終わったら、上部の「Save」ボタンを押下してください。

ローカル実行を実装する
「app-start/local」ディレクトリにある、 「index.ts」を編集していきます。
「app-start/local」ディレクトリは、インテントハンドラを持つローカル実行アプリプロジェクトになります。
Local Home SDKでは、Google Homeデバイス(Nest Mini)が、ローカルネットワーク上の未検証のデバイスを検出すると、IDENTIFYハンドラをトリガーします。
Google Homeデバイス(Nest Mini)にコマンドが送信されると、EXECUTEハンドラをトリガーします。
そのため、IDENTIFYハンドラとEXECUTEハンドラを 「index.ts」に実装します。
IDENTIFYハンドラは、identifyHandlerの「// TODO: Implement device identification」の箇所に、EXECUTEハンドラは、executeHandlerの「// TODO: Implement local execution」の箇所に実装します。
class LocalExecutionApp {
constructor(private readonly app: App) { }
identifyHandler(request: IntentFlow.IdentifyRequest):
Promise<IntentFlow.IdentifyResponse> {
// TODO: Implement device identification
}
executeHandler(request: IntentFlow.ExecuteRequest):
Promise<IntentFlow.ExecuteResponse> {
// TODO: Implement local execution
}
...
}
2019/11/20にLocal Home SDKが更新されました。identifyHandlerのリクエストデータの構造が変わっています。
詳細はLocal Home SDKのCHANGELOG.mdに記載があります。
コードラボでは前のバージョンのままなので、「app-start/local」ディレクトリにある、「package.json」を修正しておきます。
{
...
"devDependencies": {
"@google/local-home-sdk": "^0.2.0",
...
}
}
identifyHandlerの実装を以下のようにします。
verificationIdの値は、前回設定したotherDeviceIdsのいずれかが一致する必要があります。
identifyHandler(request: IntentFlow.IdentifyRequest):
Promise<IntentFlow.IdentifyResponse> {
console.log("IDENTIFY intent: " + JSON.stringify(request, null, 2));
const scanData = request.inputs[0].payload.device.udpScanData;
if (!scanData) {
const err = new IntentFlow.HandlerError(request.requestId,
'invalid_request', 'Invalid scan data');
return Promise.reject(err);
}
// In this codelab, the scan data contains only local device id.
const localDeviceId = Buffer.from(scanData.data, 'hex');
const response: IntentFlow.IdentifyResponse = {
intent: Intents.IDENTIFY,
requestId: request.requestId,
payload: {
device: {
id: 'washer',
verificationId: localDeviceId.toString(),
}
}
};
console.log("IDENTIFY response: " + JSON.stringify(response, null, 2));
return Promise.resolve(response);
}
executeHandlerの実装を以下のようにします。
ここでは、HTTP経由でローカルデバイス(ピッコロボIoT)に送信します。
※ SERVER_PORTは3388で設定しています。ローカルデバイスは3388ポートで待受ける事になります。
executeHandler(request: IntentFlow.ExecuteRequest):
Promise<IntentFlow.ExecuteResponse> {
console.log("EXECUTE intent: " + JSON.stringify(request, null, 2));
const command = request.inputs[0].payload.commands[0];
const execution = command.execution[0];
const response = new Execute.Response.Builder()
.setRequestId(request.requestId);
const promises: Array<Promise<void>> = command.devices.map((device) => {
console.log("Handling EXECUTE intent for device: " + JSON.stringify(device));
// Convert execution params to a string for the local device
const params = execution.params as IWasherParams;
const payload = this.getDataForCommand(execution.command, params);
// Create a command to send over the local network
const radioCommand = new DataFlow.HttpRequestData();
radioCommand.requestId = request.requestId;
radioCommand.deviceId = device.id;
radioCommand.data = JSON.stringify(payload);
radioCommand.dataType = 'application/json';
radioCommand.port = SERVER_PORT;
radioCommand.method = Constants.HttpOperation.POST;
radioCommand.isSecure = false;
console.log("Sending request to the smart home device:", payload);
return this.app.getDeviceManager()
.send(radioCommand)
.then(() => {
const state = {online: true};
response.setSuccessState(device.id, Object.assign(state, params));
console.log(`Command successfully sent to ${device.id}`);
})
.catch((e: IntentFlow.HandlerError) => {
e.errorCode = e.errorCode || 'invalid_request';
response.setErrorState(device.id, e.errorCode);
console.error('An error occurred sending the command', e.errorCode);
});
});
return Promise.all(promises)
.then(() => {
return response.build();
})
.catch((e) => {
const err = new IntentFlow.HandlerError(request.requestId,
'invalid_request', e.message);
return Promise.reject(err);
});
}
localディレクトリに移動し、npmで依存ライブラリを取得し、コンパイルをします。
cd local npm install npm run build
次のファイルが、「public/local-home」ディレクトリに配置されます。
- bundle.js
コンパイル済みのローカルアプリと依存関係を含むJavaScriptで出力されたファイル。 - index.html
デバイスのテスト用にアプリを提供するために使用されるローカルホスティングページ。Actionsコンソールの「Test > On device testing」で設定したURLにホストされる。
下記コマンドを実行し、Firebase Hostingにデプロイします。
firebase deploy --only hosting
仮想スマートウォッシャーを起動する
「smarthome-local/virtual-device」ディレクトリ以下にある、テスト用に用意されている仮想スマートウォッシャーを起動します。
※ 最終的にはピッコロボIoTがこれに置き換わるようにします。
「virtual-device」ディレクトリに移動し、依存ライブラリを取得します。
cd virtual-device npm install
実行時に下記パラメータを指定します。
| パラメータ | 値 |
|---|---|
| deviceId | deviceid123 |
| discoveryPortOut | 3311 |
| discoveryPacket | HelloLocalHomeSDK |
| projectId | 自分のプロジェクトID |
下記コマンドを実行します。
「<project-id>」は自分のプロジェクトIDに置き換えてください。
npm start -- \ --deviceId=deviceid123 --projectId=<project-id> \ --discoveryPortOut=3311 --discoveryPacket=HelloLocalHomeSDK
ローカル実行されているか確認する
Google Homeデバイス(Nest Mini)がローカルネットワーク経由で仮想デバイス(仮想スマートウォッシャー)にコマンド実行できているか確認します。
まずはGoogle Homeデバイス(Nest Mini)を再起動します。
このステップにより、デバイスはHTMLのURLと、Actionsコンソールに配置したスキャン構成を取得できます。
Chromeブラウザで新しいタブを開き、「chrome://inspect」をアドレスフィールドに入力してChromeインスペクターを起動します。
デバイスのリストが表示され(少し時間がかかります)、使用しているGoogle Homeデバイス(Nest Mini)の名前の下に、アプリのURLが表示されていると思います。

アプリのURLの下にある「inspect」をクリックして、Chrome DevToolsを起動します。
「コンソール」タブを選択し、TypeScriptアプリによって出力された、IDENTIFYインテントの内容が表示されることを確認します。

Google Homeデバイス(Nest Mini)へ下記のように話し、音声コマンドを介してコマンドを仮想デバイス(仮想スマートウォッシャー)に送信します。
話しかける時は、「OK、Google。ウォッシャーをオンにして」といった感じで実行できます。
※ 「ウォッシャー」じゃなく「洗濯機」でも操作できます。
- ウォッシャーをオンにして
- ウォッシャーをスタートして
- ウォッシャーを一時停止
- ウォッシャーを再開して
- ウォッシャーの動作を止めて
- ウォッシャーをオフにして
TypeScriptアプリによって出力された、EXECUTEインテントの内容が表示されることを確認します。

仮想デバイス(仮想スマートウォッシャー)の方では、以下のような出力がされていることを確認します。
※ ついでに、Web UI側も同期していることが確認できると思います。
info: {
"on": true
}
info: ***** The washer is STOPPED *****
info: Report State successful
info: {
"isRunning": true
}
info: ***** The washer is RUNNING *****
info: Report State successful
info: {
"isRunning": false
}
info: ***** The washer is STOPPED *****
info: Report State successful
info: {
"on": false
}
info: ***** The washer is OFF *****
これでローカル実行ができるようになりました。
次回は仮想デバイス(仮想スマートウォッシャー)を、ピッコロボIoTに置き換えるように実装していこうと思います!
