Internet of Things(IoT)の開発者は、Smart Home 統合を使用することで、直接音声コマンドを通じて Google アシスタントから遠隔制御できるデバイスを構築できます。これは、Google アシスタントとあなたのサーバーとの間のクラウドとクラウドの統合によって実現されます。
Smart Home アプリは、家庭とそのデバイス群に関するコンテキストデータを格納し提供するデータベースである Home Graph に依存しています。たとえば、 Home Graph には、さまざまなメーカーの複数のタイプのデバイス(サーモスタット、ランプ、ファン、および掃除機)を含むリビングルームのコンセプトを保存できます。この情報は、適切なコンテキストに基づいて、ユーザーのリクエストを実行するためにGoogleアシスタントに渡されます。
このコードラボでは、独自のクラウド統合を作成し、Google アシスタントをスマートな洗濯機に接続します。
Actions console の Overview screen 上で、Home control をクリックし、そして Smart home をクリックします。
Firebase Command Line Interface (CLI) を使用すると、ウェブアプリをローカルでサーブし、そのウェブアプリを Firebase ホスティングにデプロイできます。
CLIをインストールするために、以下の npm コマンドを実行してください:
npm -g install firebase-tools
CLI が正しくインストールされたかどうかを検証するために、コンソールを開いて、以下を実行してください:
firebase --version
Firebase CLI のバージョンが 3.3.0 以上かどうかを確認してください。
次のコマンドを実行して、Firebase CLI を認可します:
firebase login
以下のリンクをクリックして、このコードラボのサンプルをあなたの開発マシンにダウンロードしてください。
...もしくは、コマンドラインで GitHub リポジトリを close することができます:
$ git clone https://github.com/googlecodelabs/smarthome-washer.git
ダウンロードした zip ファイルを展開します。
あなたが washer-start
ディレクトリにいることを確認し、その後あなたの Firebase Project を使用するために Firebase CLI をセットアップします:
cd washer-start
firebase use --add
その後、あなたの Project ID を選択して、指示に従ってください。
washer-start
内の functions
フォルダに移動し、npm install
を実行します。
cd functions
npm install
依存関係をインストールしてプロジェクトを設定したので、いよいよアプリを実行する準備が整いました。
cd ../
firebase deploy
これは、あなたが見るべきコンソールへの出力です:
...
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/<project-id>/overview
Hosting URL: https://<project-id>.firebaseapp.com
ウェブアプリは、 https://<project-id>.firebaseapp.com
の形式のホスティングURLにて、現在サーブされているはずです。
このコマンドはまた、Firebase にいくつかの Cloud functions をデプロイしているでしょう。Actions on Google console にて、これらの URL を使う時です。
左側の Actions を選択して、 ADD YOUR FIRST ACTION をクリックします。Smart Home インテントのためのフルフィルメントを提供するバックエンドサーバのURLを入力して、その後 DONE をクリックします。
https://us-central1-<project-id>.cloudfunctions.net/smarthome
その後、DONE をクリックします。
サイドバーにて、Account Linking オプションを選択します。アカウント作成は No を選択し、そしてリンク種別が OAuth および Authentication code であることを確認してください。
以下のクライアント情報を入力します。
Client ID | ABC123 |
Client secret | DEF456 |
Authorization URL | https://us-central1-<project-id>.cloudfunctions.net/fakeauth |
Token URL | https://us-central1-<project-id>.cloudfunctions.net/faketoken |
その後、この情報を保存するために、下にある SAVE ボタンを選択し、Overview ページに戻ります。
Simulator ページを開き、あなたのプロジェクトのセットアップを完了するために、START TESTING ボタンをクリックします。
セットアップを完了するために、あなたがデプロイした functions である Smart Home クラウドにあなたのGoogleアカウントをリンクする必要があります。
SYNC
リクエストを送信して、あなたのサーバがユーザのデバイスリストを提供することを依頼します。まだデバイスを追加していないため、デバイスは見つかりません。Smart Home アカウントが Google アシスタントに接続されたので、デバイスの追加やデータの送信を開始することができます。あなたのサーバーが smarthome
関数で扱う必要のある3つのインテントがあります。
デバイスのリストを動的に取得するために、Cloud Functions for Firebase を使用することができます。それはあなたが定義可能な応答でこれら3つのインテントを処理します。洗濯機の状態をホストするために、Firebase Realtime Database を使用することができます。
functions/index.js を開きます。これには、Google アシスタントからのリクエストに応答するコードが含まれています。まず、洗濯機が応答するために、SYNC インテントを処理する必要があります。同じ応答をいつも与えることを、今見てみましょう。
app.onSync(body => {
return {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
payload: {
agentUserId: '123',
devices: []
}
};
});
今、devices
配列には、何も入っていません。washer を表現するために、その配列に新しい項目を追加します。
app.onSync(body => {
return {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
payload: {
agentUserId: '123',
devices: [{
id: 'washer',
type: 'action.devices.types.WASHER',
traits: [
'action.devices.traits.OnOff',
'action.devices.traits.StartStop',
'action.devices.traits.RunCycle'
],
name: {
defaultNames: 'My Washer'],
name: 'Washer',
nicknames: ['Washer']
},
deviceInfo: {
manufacturer: 'Acme Co',
model: 'acme-washer',
hwVersion: '1.0',
swVersion: '1.0.1'
},
attributes: {
pausable: true
}
}]
}
};
});
では、更新された関数をデプロイします。
firebase deploy
新しい SYNC 応答をテストするために、統合のリンクを解除し、再度リンクする必要があります。後で、アカウントのリンクを解除せずに SYNC 要求を発生できるように、Request Sync 機能を追加します。
SYNC
リクエストを送信します。あなたの washer が見えるはずです。今、洗濯機の現在の状況をユーザに知らせる、あるいはそれを制御するための2つのインテントを処理しなければなりません。最初に、EXECUTE インテントから追加しましょう。
Firebase console に移動し、プロジェクトを選択します。次に、Database ページを開いて、Realtime Database を使用するために GET STARTED を選択します。コンソールを使用して、 washer
という新しい子を追加します。washer の下で、トレイトの値を保存するために子供が追加されます。
washer
│
├┬ OnOff
│└─ on: false
├┬ RunCycle
│└─ dummy: false
├┬ StartStop
│├─ isPaused: false
│└─ isRunning: false
functions/index.js
内で、以下のように EXECUTE ハンドラを編集します:
app.onExecute((body) => {
const {requestId} = body;
const payload = {
commands: [{
ids: [],
status: 'SUCCESS',
states: {
online: true,
},
}],
};
for (const input of body.inputs) {
for (const command of input.payload.commands) {
for (const device of command.devices) {
const deviceId = device.id;
payload.commands[0].ids.push(deviceId);
for (const execution of command.execution) {
const execCommand = execution.command;
const {params} = execution;
switch (execCommand) {
case 'action.devices.commands.OnOff':
firebaseRef.child(deviceId).child('OnOff').update({
on: params.on,
});
payload.commands[0].states.on = params.on;
break;
case 'action.devices.commands.StartStop':
firebaseRef.child(deviceId).child('StartStop').update({
isRunning: params.start,
});
payload.commands[0].states.isRunning = params.start;
break;
case 'action.devices.commands.PauseUnpause':
firebaseRef.child(deviceId).child('StartStop').update({
isPaused: params.pause,
});
payload.commands[0].states.isPaused = params.pause;
break;
}
}
}
}
}
return {
requestId: requestId,
payload: payload,
};
});
上記の実装では、各コマンドを反復し、Firebase 内の値を更新してから、デバイスの現在の状態を示すペイロードで応答します。
では、更新された関数をデプロイします。
firebase deploy
これで、音声コマンドを入力すると値の変化を確認できます。あなたはこれらのコマンドを与えるためにあなたの携帯電話を使用することができます。
"Turn on my washer"
"Pause my washer"
"Stop my washer"
疑問の質問をサポートするために、"Is my washer on?" のように、QUERY インテントを実装する必要があります。
QUERY インテントは、デバイスのセットを含みます。各デバイスにて、それの現在の状況を応答する必要があります。
functions/index.js
内で、QUERY ハンドラを以下のように編集します:
app.onQuery((body) => {
const {requestId} = body;
const payload = {
devices: {},
};
const queryPromises = [];
for (const input of body.inputs) {
for (const device of input.payload.devices) {
const deviceId = device.id;
queryPromises.push(queryDevice(deviceId)
.then((data) => {
// Add response to device payload
payload.devices[deviceId] = data;
}
));
}
}
// Wait for all promises to resolve
return Promise.all(queryPromises).then((values) => ({
requestId: requestId,
payload: payload,
})
);
});
これで、質問を尋ねることによって、あなたの洗濯機の現在の状況を知ることができます。
"Is my washer on?"
"Is my washer running?"
今、3つ全てのインテントを実装したので、あなたは洗濯機に追加の機能を実装することができます。
モードとトグルを使用すると、開発者によって定義された名前でデバイスの特定のコンポーネントを制御することができます。このコードラボでは、洗濯機は洗濯物のサイズを小さくするか大きくするかを定義するモードを備えています。
開始するのですが、モードを表示するために index.html のセクションのコメントを外します:
<div id='demo-washer-modes-main'>
<label>Washer Mode</label>
<br>
<label id="demo-washer-modes-small" class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="demo-washer-modes-small-in">
<input checked class="mdl-radio__button" id="demo-washer-modes-small-in" name="load" type="radio"
value="on">
<span class="mdl-radio__label">Small</span>
</label>
<label id="demo-washer-modes-large" class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="demo-washer-modes-large-in">
<input class="mdl-radio__button" id="demo-washer-modes-large-in" name="load" type="radio" value="off">
<span class="mdl-radio__label">Large</span>
</label>
<br>
<br>
</div>
UPDATE ボタンを押した際に、モードが Firebase に格納されます。
SYNC 応答にて、この新しいモードについての情報を追加する必要があります。これは、以下のレスポンスに示されるように、 attributes
オブジェクトにて表示されます。
app.onSync(body => {
return {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
payload: {
agentUserId: '123',
devices: [{
id: 'washer',
type: 'action.devices.types.WASHER',
traits: [
'action.devices.traits.OnOff',
'action.devices.traits.StartStop',
'action.devices.traits.RunCycle',
'action.devices.traits.Modes',
],
name: {
defaultNames: ['My Washer'],
name: 'Washer',
nicknames: ['Washer']
},
deviceInfo: {
manufacturer: 'Acme Co',
model: 'acme-washer',
hwVersion: '1.0',
swVersion: '1.0.1'
},
attributes: {
pausable: true,
availableModes: [{
name: 'load',
name_values: [{
name_synonym: ['load'],
lang: 'en'
}],
settings: [{
setting_name: 'small',
setting_values: [{
setting_synonym: ['small'],
lang: 'en'
}]
}, {
setting_name: 'large',
setting_values: [{
setting_synonym: ['large'],
lang: 'en'
}]
}],
ordered: true
}]
}
}]
}
};
});
EXECUTE インテントにて、以下のように action.devices.commands.SetModes
コマンドを追加する必要があります。
app.onExecute((body) => {
const {requestId} = body;
const payload = {
commands: [{
ids: [],
status: 'SUCCESS',
states: {
online: true,
},
}],
};
for (const input of body.inputs) {
for (const command of input.payload.commands) {
for (const device of command.devices) {
const deviceId = device.id;
payload.commands[0].ids.push(deviceId);
for (const execution of command.execution) {
const execCommand = execution.command;
const {params} = execution;
switch (execCommand) {
case 'action.devices.commands.OnOff':
firebaseRef.child(deviceId).child('OnOff').update({
on: params.on,
});
payload.commands[0].states.on = params.on;
break;
case 'action.devices.commands.StartStop':
firebaseRef.child(deviceId).child('StartStop').update({
isRunning: params.start,
});
payload.commands[0].states.isRunning = params.start;
break;
case 'action.devices.commands.PauseUnpause':
firebaseRef.child(deviceId).child('StartStop').update({
isPaused: params.pause,
});
payload.commands[0].states.isPaused = params.pause;
break;
case 'action.devices.commands.SetModes':
firebaseRef.child(deviceId).child('Modes').update({
load: params.updateModeSettings.load,
});
break;
}
}
}
}
}
return {
requestId: requestId,
payload: payload,
};
});
今、あなたは洗濯機にモードをセットするためにコマンドを与えることができます。
"Set the washer to a large load"
最終的に、あなたは洗濯機の現在の状況についての質問に答えるために、QUERY レスポンスを更新する必要があります。モードを取得するために、queryFirebase
および queryDevice
関数に更新された変更を追加します。
const queryFirebase = (deviceId) => firebaseRef.child(deviceId).once('value')
.then((snapshot) => {
const snapshotVal = snapshot.val();
return {
on: snapshotVal.OnOff.on,
isPaused: snapshotVal.StartStop.isPaused,
isRunning: snapshotVal.StartStop.isRunning,
load: snapshotVal.Modes.load,
};
});
const queryDevice = (deviceId) => queryFirebase(deviceId).then((data) => ({
on: data.on,
isPaused: data.isPaused,
isRunning: data.isRunning,
currentRunCycle: [{
currentCycle: 'rinse',
nextCycle: 'spin',
lang: 'en',
}],
currentTotalRemainingTime: 1212,
currentCycleRemainingTime: 301,
currentModeSettings: {
load: data.load,
},
}));
以下のようにして、あなたの洗濯機について質問を尋ねることができます:
"Is my washer small load?"
トグルは、洗濯機がターボモードであるかどうかなど、true/false の状態を持つデバイスの名前付きアスペクトを表します。
開始するのですが、モードを表示するために index.html
のセクションのコメントを外します。UPDATE ボタンを押した際には、Firebase にモードが格納されます。
<div id='demo-washer-toggles-main'>
<label id="demo-washer-toggles" class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="demo-washer-toggles-in">
<input type="checkbox" id="demo-washer-toggles-in" class="mdl-switch__input">
<span class="mdl-switch__label">Is in Turbo</span>
</label>
</div>
SYNC レスポンスでは、この新しいモードに関する情報を追加する必要があります。これは、以下のレスポンスに示すように、attributes
オブジェクトに表示されます。
app.onSync(body => {
return {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
payload: {
agentUserId: '123',
devices: [{
id: 'washer',
type: 'action.devices.types.WASHER',
traits: [
'action.devices.traits.OnOff',
'action.devices.traits.StartStop',
'action.devices.traits.RunCycle',
'action.devices.traits.Modes',
'action.devices.traits.Toggles',
],
name: {
defaultNames: ['My Washer'],
name: 'Washer',
nicknames: ['Washer']
},
deviceInfo: {
manufacturer: 'Acme Co',
model: 'acme-washer',
hwVersion: '1.0',
swVersion: '1.0.1'
},
attributes: {
pausable: true,
availableModes: [{
name: 'load',
name_values: [{
name_synonym: ['load'],
lang: 'en'
}],
settings: [{
setting_name: 'small',
setting_values: [{
setting_synonym: ['small'],
lang: 'en'
}]
}, {
setting_name: 'large',
setting_values: [{
setting_synonym: ['large'],
lang: 'en'
}]
}],
ordered: true
}],
availableToggles: [{
name: 'Turbo',
name_values: [{
name_synonym: ['turbo'],
lang: 'en'
}]
}]
}
}]
}
};
});
あなたの EXECUTE インテントにおいて、以下に示すように、 action.devices.commands.SetToggles
コマンドを追加する必要があります。
app.onExecute((body) => {
const {requestId} = body;
const payload = {
commands: [{
ids: [],
status: 'SUCCESS',
states: {
online: true,
},
}],
};
for (const input of body.inputs) {
for (const command of input.payload.commands) {
for (const device of command.devices) {
const deviceId = device.id;
payload.commands[0].ids.push(deviceId);
for (const execution of command.execution) {
const execCommand = execution.command;
const {params} = execution;
switch (execCommand) {
case 'action.devices.commands.OnOff':
firebaseRef.child(deviceId).child('OnOff').update({
on: params.on,
});
payload.commands[0].states.on = params.on;
break;
case 'action.devices.commands.StartStop':
firebaseRef.child(deviceId).child('StartStop').update({
isRunning: params.start,
});
payload.commands[0].states.isRunning = params.start;
break;
case 'action.devices.commands.PauseUnpause':
firebaseRef.child(deviceId).child('StartStop').update({
isPaused: params.pause,
});
payload.commands[0].states.isPaused = params.pause;
break;
case 'action.devices.commands.SetModes':
firebaseRef.child(deviceId).child('Modes').update({
load: params.updateModeSettings.load,
});
break;
case 'action.devices.commands.SetToggles':
firebaseRef.child(deviceId).child('Toggles').update({
Turbo: params.updateToggleSettings.Turbo,
});
break;
}
}
}
}
}
return {
requestId: requestId,
payload: payload,
};
});
これで、あなたは洗濯機のモードをセットするためにコマンドを与えることができます。
"Turn on turbo for the washer"
最終的に、あなたは洗濯機のターボモードについての質問に答えるために、QUERY レスポンスを更新する必要があります。トグルの状況を取得するために、queryFirebase および queryDevice 関数に更新された変更を追加します。
const queryFirebase = (deviceId) => firebaseRef.child(deviceId).once('value')
.then((snapshot) => {
const snapshotVal = snapshot.val();
return {
on: snapshotVal.OnOff.on,
isPaused: snapshotVal.StartStop.isPaused,
isRunning: snapshotVal.StartStop.isRunning,
load: snapshotVal.Modes.load,
turbo: snapshotVal.Toggles.Turbo,
};
});
const queryDevice = (deviceId) => queryFirebase(deviceId).then((data) => ({
on: data.on,
isPaused: data.isPaused,
isRunning: data.isRunning,
currentRunCycle: [{
currentCycle: 'rinse',
nextCycle: 'spin',
lang: 'en',
}],
currentTotalRemainingTime: 1212,
currentCycleRemainingTime: 301,
currentModeSettings: {
load: data.load,
},
currentToggleSettings: {
Turbo: data.turbo,
},
}));
あなたは以下のように、洗濯機についての質問を尋ねることができます。
"Is my washer in turbo mode?"
今、洗濯機は完全に実装されています。あなたは、この洗濯機の現在の状況を得て、制御することができます。しかしながら、新しい機能を追加する度に、あなたは統合のリンク解除を行い、そして再リンクしなければなりませんでした。
SYNC リクエストは、このリンク手順の間であなたのサーバに届くのみです。Request Sync APIの追加によって、SYNCリクエストをトリガーすることができ、ユーザアカウントのリンク解除および再リンクを行うことなしに、ユーザのデバイスリストを更新することが可能になります。
Request Sync APIは、パラメータとしてユーザIDを受け取ります。これは、あなたのサーバにおいてユーザを示すユーザIDであるべきです。このコードラボでは、その値は "123" とハードコードされます。
const app = smarthome({
API_KEY: '<api-key>'
});
フロントエンドウェブ UI にて、ヘッダにリフレッシュアイコンがあります。Request Sync 呼び出しをさせるために、クリックリスナーを UI に追加することができます。
main.js
を以下のようにします:
this.requestSync = document.getElementById('request-sync');
this.requestSync.addEventListener('click', () => {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.log("Request SYNC success!");
}
};
xhttp.open("POST", "https://us-central1-<project-id>.cloudfunctions.net/requestsync", true);
xhttp.send();
});
これで、そのアイコンをクリックすると、あなたのサーバのログに SYNC リクエストが出てきます。デバイスを更新する、新しいデバイスを追加する、またはこのデバイスを削除する度に、新しい Request Sync 呼び出しが送信されて、その際にはデバイスの一覧が表示されるのがわかります。
応答はまた、あなたのログに表示されます。そのログは、Firebase を通じて見ることができます。
Report State API を使って、Smart Home 統合は Home graph にデバイスの状況を積極的に送信することができます。これにより、ユーザの問い合わせを迅速に完了でき、そして携帯端末やスマートディスプレイ上のリッチな UI を使って、全てのユーザのデバイスの状況を知ることができるようになります。
洗濯機の状況を変更するために UPDATE ボタンをクリックした際に、あなたは Home graph にこれをレポートする追加の手順を加えることができます。
私たちの関数を書くには、データが安全に送信されることを確認する必要があります。これはJWT (JSON web tokens) を介して行われます。このコードラボでは、このトークンの作成を容易にしてくれる googleapis npm 依存ライブラリを使用します。
あなたのプロジェクトの Google Cloud Console に行きます。APIs & Services セクション内の Credentials を選択します。Create Credentials ボタンをクリックし、そして Service account key を選択します。Role 設定のために、Project -> Editor を選択します。
Service account の作成後に、JSON ファイルをダウンロードします。このファイルを、プロジェクト内の functions フォルダに key.json
という名前で保存してください。
これは、Firebase データベーストリガーを使用して行うことができます。特定の書き込みイベントが発生すると、自動的に状態を報告することができます。
functions/index.js
内を以下のようにします。
exports.reportstate = functions.database.ref('{deviceId}').onWrite((event) => {
console.info('Firebase write event triggered this cloud function');
const https = require('https');
const {google} = require('googleapis');
const key = require('./key.json');
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/homegraph'],
null
);
const snapshotVal = event.data.val();
const postData = {
requestId: 'ff36a3cc', /* Any unique ID */
agentUserId: '123', /* Hardcoded user ID */
payload: {
devices: {
states: {
/* Report the current state of our washer */
[event.params.deviceId]: {
on: snapshotVal.OnOff.on,
isPaused: snapshotVal.StartStop.isPaused,
isRunning: snapshotVal.StartStop.isRunning,
},
},
},
},
};
jwtClient.authorize((err, tokens) => {
if (err) {
console.error(err);
return;
}
const options = {
hostname: 'homegraph.googleapis.com',
port: 443,
path: '/v1/devices:reportStateAndNotification',
method: 'POST',
headers: {
Authorization: ` Bearer ${tokens.access_token}`,
},
};
return new Promise((resolve, reject) => {
let responseData = '';
const req = https.request(options, (res) => {
res.on('data', (d) => {
responseData += d.toString();
});
res.on('end', () => {
resolve(responseData);
});
});
req.on('error', (e) => {
reject(e);
});
// Write data to request body
req.write(JSON.stringify(postData));
req.end();
}).then((data) => {
console.info(data);
});
});
});
おめでとうございます!あなたは Smart Home を使った自身のデバイスを Google アシスタントに統合することに成功しました。
ここに、あなたがより深く実装することができるいくつのアイディアがあります。
もしあなたがデバイスメーカーの方だった場合は、デバイスを Google アシスタントに統合した後、審査のためにあなたのアクションを提出できます。あなたの統合は、認証プロセスを経て提出されます。