智能家居 Action 使用设备类型来让 Google 助理了解对某个设备应该使用哪种语法。设备特征定义各个设备类型的功能。设备可继承添加到 Action 的每个设备特征的状态。
您可以将任何支持的特征关联到您选择的设备类型,以便自定义用户设备的功能。如果您想在 Action 中实现目前设备架构中并未提供的自定义特征,则 Modes 和 Toggle 特征允许使用您定义的自定义名称来控制特定设置。
除了按类型和特征提供的基本控制功能以外,Smart Home API 还提供其他用于完善用户体验的功能。当 intent 未成功实现时,错误响应可提供详细的用户反馈。双重身份验证 (2FA) 可以扩展这些响应,并增强您所选设备特征的安全性。通过发送特定错误响应验证 Google 助理发出的阻止后,您的智能家居 Action 可能需要其他授权才能完成相应命令。
前提条件
- 创建智能家居 Action 开发者指南
- 智能家居洗衣机 Codelab
- 设备类型和特性开发者指南
构建内容
在本 Codelab 中,您将利用 Firebase 部署一个预构建的智能家居集成,然后学习如何向智能家居洗衣机添加针对负荷容量和涡轮模式的非标准特征。您还将实现错误和异常报告,并学习如何使用 2FA 强制执行语音确认以开启洗衣机。
学习内容
- 如何向 Action 添加“Modes”和“Toggles”特征
- 如何报告错误和异常
- 如何应用 2FA
所需条件
- 网络浏览器,例如 Google Chrome
- 安装了 Google Home 应用的 iOS 或 Android 设备
- Node.js 10.16 或更高版本
- Google 帐号
- Google Cloud 结算帐号
启用活动控件
在您计划与 Google 助理搭配使用的 Google 帐号中启用以下活动控件:
- 网络与应用活动记录
- 设备信息
- 语音和音频活动记录
创建 Actions 项目
- 转到 Actions on Google 开发者控制台。
- 点击 New Project,输入项目名称,然后点击 CREATE PROJECT。
选择智能家居应用
在 Actions 控制台的“Overview”屏幕中,选择 Smart home。
选择 Smart home 体验卡片,然后系统会将您转到项目控制台。
安装 Firebase CLI
借助 Firebase 命令行界面 (CLI),您可以在本地提供 Web 应用,并将您的 Web 应用部署到 Firebase Hosting。
如需安装 CLI,请从终端运行以下 npm 命令:
npm install -g firebase-tools
如需验证 CLI 是否已正确安装,请运行以下命令:
firebase --version
运行以下命令,授权您的 Google 帐号使用 Firebase CLI:
firebase login
启用 HomeGraph API
借助 HomeGraph API,您可以在用户 Home Graph 中存储并查询设备及其状态。如需使用此 API,您必须先打开 Google Cloud Console,然后启用 HomeGraph API。
在 Google Cloud Console 中,请务必选择与您的 Actions <project-id>.
相匹配的项目。然后,在 HomeGraph API 的 API 库屏幕中,点击启用。
开发环境设置完毕后,您可以部署入门级项目以验证所有设置是否已配置正确。
获取源代码
点击以下链接,将此 Codelab 的示例下载到您的开发机器上:
…或者,您也可以通过命令行克隆 GitHub 代码库:
git clone https://github.com/googlecodelabs/smarthome-traits.git
解压下载的 ZIP 文件。
项目简介
入门级项目包含以下子目录:
public:
一种前端界面,可轻松地控制和监控智能洗衣机的状态。functions:
一种已全面实现的云服务,可使用 Cloud Functions for Firebase 和 Firebase Realtime Database 来管理智能洗衣机。
提供的云执行方式在 index.js
中包括以下函数:
fakeauth
:用于帐号关联的授权端点faketoken
:用于帐号关联的令牌端点smarthome
:智能家居 intent 执行方式端点reportstate
:在设备状态改变时调用 Home Graph APIrequestsync
:允许用户设备进行更新,而无需重新关联帐号
关联到 Firebase
转到 washer-start
目录,然后使用您的 Actions 项目设置 Firebase CLI:
cd washer-start firebase use <project-id>
部署到 Firebase
转到 functions
文件夹,然后使用 npm.
安装所有必要的依赖项
cd functions npm install
依赖性安装完毕且配置好项目后,您就可以首次运行此应用了。
firebase deploy
您应该会看到以下控制台输出:
... ✔ Deploy complete! Project Console: https://console.firebase.google.cn/project/<project-id>/overview Hosting URL: https://<project-id>.firebaseapp.com
此命令会部署一个 Web 应用以及几个 Cloud Functions for Firebase。
在浏览器 (https://<project-id>.firebaseapp.com
) 中打开托管网址以查看此 Web 应用。您会看到以下界面:
此网络界面表示用于查看或修改设备状态的第三方平台。如需使用设备信息填充数据库,请点击 UPDATE。此页面不会显示任何更改,但洗衣机的当前状态会存储在数据库中。
现在,您可以通过 Actions 控制台将您部署的云服务关联到 Google 助理。
配置您的 Actions 控制台项目
在 Overview > Build your Action 下,选择 Add Action(s)。输入为智能家居 intent 提供执行方式的 Cloud Functions 函数的网址,然后点击 Save。
https://us-central1-<project-id>.cloudfunctions.net/smarthome
在 Develop > Invocation 标签页中,在 Display name 中为您的 Action 添加显示名,然后点击 Save。此名称会显示在 Google Home 应用中。
如需启用帐号关联,请在左侧导航栏中依次选择 Develop > Account linking 选项。使用以下帐号关联设置:
客户端 ID |
|
客户端密钥 |
|
授权网址 |
|
令牌网址 |
|
点击 Save 保存您的帐号关联配置,然后点击 Test 针对您的项目启用测试。
系统会将您重定向到 Simulator。将鼠标指针悬停在“Testing on Device”() 图标上,以便验证是否已针对您的项目启用测试。
关联到 Google 助理
为了测试您的智能家居 Action,您需要将项目与 Google 帐号相关联。这样一来,您就可以使用登录同一帐号的 Google 助理界面和 Google Home 应用来进行测试。
- 在手机上打开 Google 助理设置。请注意,您登录的帐号应该与控制台所用的帐号相同。
- 依次转到 Google 助理 > 设置 > 家居控制(位于“Google 助理”下方)。
- 点击右下角的加号 (+) 图标。
- 您应该会看到带有 [test] 前缀及您设置的显示名的测试应用。
- 选择此项目。然后,Google 助理会通过您的服务进行身份验证并发送
SYNC
请求,以要求您的服务提供用户设备列表。
打开 Google Home 应用,然后验证您能否看到相应洗衣机设备。
验证您是否可以在 Google Home 应用中通过语音指令控制洗衣机。您还应该会在云执行方式的前端网络界面中看到设备状态更改情况。
现在,您已经部署了一个基础洗衣机,接下来,您可以自定义设备上的可用模式。
a``ction.devices.traits.Modes
特征可让设备针对一种模式提供任意数量的设置(一次只能设定一项设置)。您可以向洗衣机添加模式,以便定义洗衣负荷容量:小、中或大。
更新 SYNC 响应
您需要向 functions/index.js
中的 SYNC
响应添加新特征的相关信息。这些数据会显示在 traits
数组和 attributes
对象中,如以下代码段所示。
index.js
app.onSync(body => {
return {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
payload: {
agentUserId: USER_ID,
devices: [{
id: 'washer',
type: 'action.devices.types.WASHER',
traits: [
'action.devices.traits.OnOff',
'action.devices.traits.StartStop',
'action.devices.traits.RunCycle',
// Add Modes trait
'action.devices.traits.Modes',
],
name: { ... },
deviceInfo: { ... },
attributes: {
pausable: true,
//Add availableModes
availableModes: [{
name: 'load',
name_values: [{
name_synonym: ['load'],
lang: 'en',
}],
settings: [{
setting_name: 'small',
setting_values: [{
setting_synonym: ['small'],
lang: 'en',
}]
}, {
setting_name: 'medium',
setting_values: [{
setting_synonym: ['medium'],
lang: 'en',
}]
}, {
setting_name: 'large',
setting_values: [{
setting_synonym: ['large'],
lang: 'en',
}]
}],
ordered: true,
}],
},
}],
},
};
});
添加新的 EXECUTE intent 命令
在 EXECUTE
intent 中添加 action.devices.commands.SetModes
命令,如以下代码段所示。
index.js
const updateDevice = async (execution,deviceId) => {
const {params,command} = execution;
let state, ref;
switch (command) {
case 'action.devices.commands.OnOff':
state = {on: params.on};
ref = firebaseRef.child(deviceId).child('OnOff');
break;
case 'action.devices.commands.StartStop':
state = {isRunning: params.start};
ref = firebaseRef.child(deviceId).child('StartStop');
break;
case 'action.devices.commands.PauseUnpause':
state = {isPaused: params.pause};
ref = firebaseRef.child(deviceId).child('StartStop');
Break;
// Add SetModes command
case 'action.devices.commands.SetModes':
state = {load: params.updateModeSettings.load};
ref = firebaseRef.child(deviceId).child('Modes');
break;
}
更新 QUERY 响应
接下来,更新 QUERY
响应,以便报告洗衣机的当前状态。
将更新后的更改添加到 queryFirebase
和 queryDevice
函数以获取存储在 Realtime Database 中的状态。
index.js
const queryFirebase = async (deviceId) => {
const snapshot = await firebaseRef.child(deviceId).once('value');
const snapshotVal = snapshot.val();
return {
on: snapshotVal.OnOff.on,
isPaused: snapshotVal.StartStop.isPaused,
isRunning: snapshotVal.StartStop.isRunning,
// Add Modes snapshot
load: snapshotVal.Modes.load,
};
}
const queryDevice = async (deviceId) => {
const data = await queryFirebase(deviceId);
return {
on: data.on,
isPaused: data.isPaused,
isRunning: data.isRunning,
currentRunCycle: [{ ... }],
currentTotalRemainingTime: 1212,
currentCycleRemainingTime: 301,
// Add currentModeSettings
currentModeSettings: {
load: data.load,
},
};
};
更新报告状态
最后,更新 reportstate
函数,以便将洗衣机的当前负荷设置报告给 Home Graph。
index.js
const requestBody = {
requestId: 'ff36a3cc', /* Any unique ID */
agentUserId: USER_ID,
payload: {
devices: {
states: {
/* Report the current state of your washer */
[context.params.deviceId]: {
on: snapshot.OnOff.on,
isPaused: snapshot.StartStop.isPaused,
isRunning: snapshot.StartStop.isRunning,
// Add currentModeSettings
currentModeSettings: {
load: snapshot.Modes.load,
},
},
},
},
},
};
部署到 Firebase
运行以下命令以部署更新后的 Action:
firebase deploy --only functions
部署完成后,转到网络界面,然后点击工具栏中的 Refresh 按钮。这会触发请求同步操作,以便 Google 助理接收更新后的 SYNC
响应数据。
现在,您可以发出一个命令来设置洗衣机的模式,例如:
“Ok Google, set the washer load to large.”(Ok Google,把洗衣机负荷容量设为大。)
此外,您还可以提出有关洗衣机的问题,例如:
“Ok Google, what is the washer load?”(Ok Google,洗衣机的负荷容量是多少?)
action.devices.traits.Toggles
特征表示设备的具有 true 或 false 状态的已命名方面,例如洗衣机是否处于涡轮模式。
更新 SYNC 响应
在 SYNC
响应中,您需要添加有关新设备特征的信息。这些信息将显示在 traits
数组和 attributes
对象中,如以下代码段所示。
index.js
app.onSync(body => {
return {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
payload: {
agentUserId: USER_ID,
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',
// Add Toggles trait
'action.devices.traits.Toggles',
],
name: { ... },
deviceInfo: { ... },
attributes: {
pausable: true,
availableModes: [{
name: 'load',
name_values: [{
name_synonym: ['load'],
lang: 'en'
}],
settings: [{ ... }],
ordered: true,
}],
//Add availableToggles
availableToggles: [{
name: 'Turbo',
name_values: [{
name_synonym: ['turbo'],
lang: 'en',
}],
}],
},
}],
},
};
});
添加新的 EXECUTE intent 命令
在 EXECUTE
intent 中添加 action.devices.commands.SetToggles
命令,如以下代码段所示。
index.js
const updateDevice = async (execution,deviceId) => {
const {params,command} = execution;
let state, ref;
switch (command) {
case 'action.devices.commands.OnOff':
state = {on: params.on};
ref = firebaseRef.child(deviceId).child('OnOff');
break;
case 'action.devices.commands.StartStop':
state = {isRunning: params.start};
ref = firebaseRef.child(deviceId).child('StartStop');
break;
case 'action.devices.commands.PauseUnpause':
state = {isPaused: params.pause};
ref = firebaseRef.child(deviceId).child('StartStop');
break;
case 'action.devices.commands.SetModes':
state = {load: params.updateModeSettings.load};
ref = firebaseRef.child(deviceId).child('Modes');
break;
// Add SetToggles command
case 'action.devices.commands.SetToggles':
state = {Turbo: params.updateToggleSettings.Turbo};
ref = firebaseRef.child(deviceId).child('Toggles');
break;
}
更新 QUERY 响应
最后,您需要更新 QUERY
响应以报告洗衣机的涡轮模式。将更新后的更改添加到 queryFirebase
和 queryDevice
函数以获取存储在 Realtime Database 中的切换状态。
index.js
const queryFirebase = async (deviceId) => {
const snapshot = await firebaseRef.child(deviceId).once('value');
const snapshotVal = snapshot.val();
return {
on: snapshotVal.OnOff.on,
isPaused: snapshotVal.StartStop.isPaused,
isRunning: snapshotVal.StartStop.isRunning,
load: snapshotVal.Modes.load,
// Add Toggles snapshot
Turbo: snapshotVal.Toggles.Turbo,
};
}
const queryDevice = async (deviceId) => {
const data = queryFirebase(deviceId);
return {
on: data.on,
isPaused: data.isPaused,
isRunning: data.isRunning,
currentRunCycle: [{ ... }],
currentTotalRemainingTime: 1212,
currentCycleRemainingTime: 301,
currentModeSettings: {
load: data.load,
},
// Add currentToggleSettings
currentToggleSettings: {
Turbo: data.Turbo,
},
};
};
更新报告状态
最后,更新 reportstate
函数,以便向洗衣机报告洗衣机是否已设为涡轮模式。
index.js
const requestBody = {
requestId: 'ff36a3cc', /* Any unique ID */
agentUserId: USER_ID,
payload: {
devices: {
states: {
/* Report the current state of your washer */
[context.params.deviceId]: {
on: snapshot.OnOff.on,
isPaused: snapshot.StartStop.isPaused,
isRunning: snapshot.StartStop.isRunning,
currentModeSettings: {
load: snapshot.Modes.load,
},
// Add currentToggleSettings
currentToggleSettings: {
Turbo: snapshot.Toggles.Turbo,
},
},
},
},
},
};
部署到 Firebase
运行以下命令以部署更新后的函数:
firebase deploy --only functions
部署完成后,点击网络界面中的 Refresh 按钮以触发请求同步。
现在,您可以说出指令,以便将洗衣机设为涡轮模式:
“Ok Google, turn on turbo for the washer.”(Ok Google,开启洗衣机的涡轮模式。)
您还可以提出以下问题,以便检查洗衣机是否已处于涡轮模式:
“Ok Google, is my washer in turbo mode?”(Ok Google,我的洗衣机开启涡轮模式了吗?)
借助智能家居 Action 中的错误处理功能,您可以在有问题导致 EXECUTE
和 QUERY
响应失败时向用户报告相关信息。这些通知可在用户与您的智能设备和 Action 互动时为用户提供更积极的用户体验。
每当 EXECUTE
或 QUERY
请求失败时,您的 Action 都应返回错误代码。例如,如果您想在用户试图在机盖处于打开状态的情况下启动洗衣机时抛出错误,那么您的 EXECUTE
响应将如以下代码段所示:
{
"requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
"payload": {
"commands": [
{
"ids": [
"456"
],
"status": "ERROR",
"errorCode": "deviceLidOpen"
}
]
}
}
现在,如果用户要求启动洗衣机,Google 助理会说出以下内容作为响应:
“The lid is open on the washer. Please close it and try again.”(洗衣机的机盖打开了。请关闭机盖,然后重试。)
异常与错误类似,但异常指示的是某个提醒与某个命令相关联的情况,这种情况未必会阻止顺利执行。异常可使用 StatusReport
特征提供相关信息,例如电池电量或近期状态更改。不会阻止执行的异常代码会随 SUCCESS
状态一起返回,而会阻止执行的异常代码会随 EXCEPTIONS
状态一起返回。
以下代码段中提供了包含异常的示例响应:
{
"requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
"payload": {
"commands": [{
"ids": ["123"],
"status": "SUCCESS",
"states": {
"online": true,
"isPaused": false,
"isRunning": false,
"exceptionCode": "runCycleFinished"
}
}]
}
}
Google 助理会说出以下内容作为响应:
“The washer has finished running.”(洗衣机已结束运行。)
如需为您的洗衣机添加错误报告,请打开 functions/index.js
,然后添加错误类别定义,如以下代码段所示:
index.js
app.onQuery(async (body) => {...});
// Add SmartHome error handling
class SmartHomeError extends Error {
constructor(errorCode, message) {
super(message);
this.name = this.constructor.name;
this.errorCode = errorCode;
}
}
更新执行响应以返回错误代码和错误状态:
index.js
const executePromises = [];
const intent = body.inputs[0];
for (const command of intent.payload.commands) {
for (const device of command.devices) {
for (const execution of command.execution) {
executePromises.push( ... )
//Add error response handling
.catch((error) => {
functions.logger.error('EXECUTE', device.id, error);
result.ids.push(device.id);
if(error instanceof SmartHomeError) {
result.status = 'ERROR';
result.errorCode = error.errorCode;
}
})
);
}
}
}
现在,Google 助理可以告知用户您报告的任何错误代码了。在下一部分中,您会看到具体示例。
如果您的设备具有任何需要安全保障或应限定特定授权用户群组使用的模式(例如软件更新或开锁),您应该在 Action 中实现双重身份验证 (2FA)。
您可以针对所有设备类型和特征实现 2FA,以便自定义是否每次都要进行安全验证,或是否需要满足特定条件。
No
challenge
- 不使用身份验证的请求和响应(这是默认行为)ackNeeded
- 需要进行显式确认(是或否)的 2FApinNeeded
- 需要输入个人识别码 (PIN) 的 2FA
在本 Codelab 中,请向用于开启洗衣机的命令添加 ackNeeded
验证,并添加在 2FA 验证失败时返回错误的功能。
打开 functions/index.js
,然后添加错误类别定义以返回错误代码和验证类型,如以下代码段所示:
index.js
class SmartHomeError extends Error { ... }
// Add 2FA error handling
class ChallengeNeededError extends SmartHomeError {
constructor(tfaType) {
super('challengeNeeded', tfaType);
this.tfaType = tfaType;
}
}
此外,您还需要更新相应的执行响应以返回 challengeNeeded
错误,具体如下所示:
index.js
const executePromises = [];
const intent = body.inputs[0];
for (const command of intent.payload.commands) {
for (const device of command.devices) {
for (const execution of command.execution) {
executePromises.push( ... )
.catch((error) => {
functions.logger.error('EXECUTE', device.id, error);
result.ids.push(device.id);
if(error instanceof SmartHomeError) {
result.status = 'ERROR';
result.errorCode = error.errorCode;
//Add 2FA error handling
if(error instanceof ChallengeNeededError) {
result.challengeNeeded = {
type: error.tfaType
};
}
}
})
);
}
}
}
最后,修改 updateDevice
以要求通过显式确认来开启或关闭洗衣机。
index.js
const updateDevice = async (execution,deviceId) => {
const {challenge,params,command} = execution; //Add 2FA challenge
let state, ref;
switch (command) {
case 'action.devices.commands.OnOff':
//Add 2FA challenge
if (!challenge || !challenge.ack) {
throw new ChallengeNeededError('ackNeeded');
}
state = {on: params.on};
ref = firebaseRef.child(deviceId).child('OnOff');
break;
...
}
return ref.update(state)
.then(() => state);
};
部署到 Firebase
运行以下命令以部署更新后的函数:
firebase deploy --only functions
部署更新后的代码后,当您要求 Google 助理开启或关闭洗衣机时,您必须使用语音确认相应 Action,具体如下所示:
您:“Ok Google, turn on the washer.”(Ok Google,开启洗衣机。)
Google 助理:“Are you sure you want to turn on the washer?”(确定要开启洗衣机吗?)
您:“Yes.”(是的。)
此外,您还可以打开 Firebase 日志以查看 2FA 流程的每个步骤的详细响应。
恭喜!您已通过 Modes
和 Toggles
特征扩展智能家居 Action 的功能,并通过 2FA 确保其能够安全执行。
了解详情
您可以实现以下想法以进行更深入的研究:
- 向您的设备添加本地执行功能。
- 使用其他 2FA 验证类型来修改设备状态。
- 更新
RunCycle
特征 QUERY 响应以进行动态更新。 - 探索本 GitHub 示例。