1. 准备工作
智能家居 Action 使用设备类型告知 Google 助理应对设备使用什么语法。设备特征定义各个设备类型的功能。设备会继承添加到 Action 的每个设备特征的状态。
您可以将任何支持的特征关联到您选择的设备类型,以便自定义用户设备的功能。如果您想在 Action 中实现设备架构中当前不支持的自定义 trait,则 Modes 和 Toggle trait 允许使用特定设置。自定义名称来自定义控件
除了按类型和特征提供的基本控制功能以外,Smart Home API 还提供其他用于完善用户体验的功能。当 intent 未成功实现时,错误响应可提供详细的用户反馈。第二层用户身份验证可以扩展这些响应,并为您选择的设备特征增加额外的安全性。通过发送特定错误响应验证 Google 助理发出的阻止后,您的智能家居 Action 可能需要其他授权才能完成相应命令。
前提条件
- 创建智能家居 Action 开发者指南
- 智能家居洗衣机 Codelab
- 设备类型和特性开发者指南
构建内容
在此 Codelab 中,您将通过 Firebase 部署预构建的智能家居集成,然后学习如何向智能家居洗衣机添加针对负载规模和涡轮模式的非标准特征。您还将实现错误和异常报告,并了解如何使用第二层用户身份验证强制执行口头确认来开启洗衣机。
学习内容
- 如何向 Action 添加“Modes”和“Toggles”特征
- 如何报告错误和异常
- 如何应用第二层用户身份验证
所需条件
- 网络浏览器,例如 Google Chrome
- 安装了 Google Home 应用的 iOS 或 Android 设备
- Node.js 10.16 或更高版本
- Google 账号
- Google Cloud 结算账号
2. 开始使用
启用活动控件
若要使用 Google 助理,你必须与 Google 分享某些活动数据。Google 助理需要使用这些数据才能正常运行;然而,分享数据的要求并非专门针对该 SDK。如果你还没有 Google 账号,可以创建一个,以便分享这些数据。你可以使用任何 Google 账号,不要求必须用你的开发者账号。
打开要与 Google 助理搭配使用的 Google 账号的“活动控件”页面。
确保已启用以下切换开关:
- 网络与应用活动记录 - 此外,请务必选中包括 Chrome 历史记录和使用 Google 服务的网站、应用和设备中的活动记录复选框。
- 设备信息
- 语音和音频活动记录
创建 Actions 项目
- 前往 Actions on Google 开发者控制台。
- 点击 New Project,输入项目名称,然后点击 CREATE PROJECT。
选择智能家居应用
在 Actions 控制台的“Overview”屏幕中,选择 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 控制台并启用 HomeGraph API。
在 Google Cloud Console 中,请务必选择与您的 Actions <project-id>.
相匹配的项目。然后,在 HomeGraph API 的 API 库屏幕中,点击启用。
3. 运行入门级应用
开发环境设置完毕后,你可以部署入门级项目以验证所有设置是否已配置正确。
获取源代码
点击以下链接,将此 Codelab 的示例下载到您的开发机器上:
…或者,您也可以通过命令行克隆 GitHub 代码库:
git clone https://github.com/google-home/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 项目
初始化 Firebase 项目。
firebase init
选择 CLI 功能:Realtime Database、Functions,以及包含 Firebase Hosting 的 Hosting 功能。
? Which Firebase CLI features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices. ❯◉ Realtime Database: Configure a security rules file for Realtime Database and (optionally) provision default instance ◯ Firestore: Configure security rules and indexes files for Firestore ◉ Functions: Configure a Cloud Functions directory and its files ◉ Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys ◯ Hosting: Set up GitHub Action deploys ◯ Storage: Configure a security rules file for Cloud Storage ◯ Emulators: Set up local emulators for Firebase products ◯ Remote Config: Configure a template file for Remote Config ◯ Extensions: Set up an empty Extensions manifest
这将初始化项目的必要 API 和功能。
在系统显示提示时,初始化 Realtime Database。你可以使用数据库实例的默认位置。
? It seems like you haven't initialized Realtime Database in your project yet. Do you want to set it up? Yes ? Please choose the location for your default Realtime Database instance: us-central1
由于你使用的是入门级项目代码,因此请为安全规则选择默认文件,并确保不覆盖现有的数据库规则文件。
? File database.rules.json already exists. Do you want to overwrite it with the Realtime Database Security Rules for <project-ID>-default-rtdb from the Firebase Console? No
如果要重新初始化项目,当系统询问您是要初始化还是覆盖代码库时,请选择覆盖。
? Would you like to initialize a new codebase, or overwrite an existing one? Overwrite
配置 Functions 时,您应该使用默认文件,并确保不会覆盖项目示例中的现有 index.js 和 package.json 文件。
? What language would you like to use to write Cloud Functions? JavaScript ? Do you want to use ESLint to catch probable bugs and enforce style? No ? File functions/package.json already exists. Overwrite? No ? File functions/index.js already exists. Overwrite? No
如果您要重新初始化项目,请在系统询问您是否要初始化或覆盖 functions/.gitignore 时选择 No。
? File functions/.gitignore already exists. Overwrite? No
? Do you want to install dependencies with npm now? Yes
最后,配置 Hosting 设置以使用项目代码中的 public
目录,并使用现有的 index.html 文件。当系统要求您使用 ESLint 时,请选择 No。
? What do you want to use as your public directory? public ? Configure as a single-page app (rewrite all urls to /index.html)? Yes ? Set up automatic builds and deploys with GitHub? No ? File public/index.html already exists. Overwrite? No
如果无意中启用了 ESLint,有两种方法可以将其停用:
- 使用 GUI,转到项目下的
../functions
文件夹,选择隐藏文件.eslintrc.js
并将其删除。不要将其误认为名称相似的.eslintrc.json
。 - 使用命令行:
cd functions rm .eslintrc.js
部署到 Firebase
依赖性安装完毕且配置好项目后,您就可以首次运行此应用了。
firebase deploy
您应该会看到以下控制台输出:
... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/<project-id>/overview Hosting URL: https://<project-id>.web.app
此命令会部署一个 Web 应用以及几个 Cloud Functions for Firebase。
在浏览器 (https://<project-id>.web.app
) 中打开托管网址以查看此 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。如果您没有看到“Test now enabled”,请点击 Reset Test 以验证是否已启用测试。
关联到 Google 助理
为了测试您的智能家居 Action,您需要将项目与 Google 账号相关联。这样一来,您就可以使用登录同一账号的 Google 助理界面和 Google Home 应用来进行测试。
- 在手机上打开 Google 助理设置。请注意,您登录的账号应该与控制台所用的账号相同。
- 依次转到 Google 助理 > 设置 > 家居控制(位于“Google 助理”下方)。
- 点击右上角的搜索图标。
- 使用 [test] 前缀搜索您的测试应用,以查找您的特定测试应用。
- 选择此项目。然后,Google 助理会通过您的服务进行身份验证并发送
SYNC
请求,以要求您的服务提供用户设备列表。
打开 Google Home 应用,然后验证您能否看到相应洗衣机设备。
验证您是否可以在 Google Home 应用中通过语音指令控制洗衣机。您还应该会在云执行方式的前端网络界面中看到设备状态更改情况。
现在,您已经部署了一个基础洗衣机,接下来,您可以自定义设备上的可用模式。
4. 添加模式
借助 action.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,洗衣机的负荷容量是多少?)
5. 添加切换开关
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,我的洗衣机开启涡轮模式了吗?)
6. 报告错误和异常
借助智能家居 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 助理可以将您报告的任何错误代码告知用户。在下一部分中,您会看到具体示例。
7. 添加第二层用户身份验证
如果您的设备具有任何需要保护的模式或应仅限特定的授权用户组使用(例如更新软件或解除锁定),您应在 Action 中实现第二层用户身份验证。
您可以针对所有设备类型和特征实现第二层用户身份验证,自定义是否每次都进行安全验证,或是否需要满足特定条件。
No
challenge
- 不使用身份验证的请求和响应(这是默认行为)ackNeeded
- 需要明确确认的第二层用户身份验证(是或否)pinNeeded
- 需要进行个人识别码 (PIN) 的第二层用户身份验证
对于此 Codelab,请在用于开启洗衣机的命令中添加 ackNeeded
质询,并加入在二次验证质询失败时返回错误的功能。
打开 functions/index.js
,然后添加错误类别定义以返回错误代码和验证类型,如以下代码段所示:
index.js
class SmartHomeError extends Error { ... }
// Add secondary user verification error handling
class ChallengeNeededError extends SmartHomeError {
/**
* Create a new ChallengeNeededError
* @param {string} suvType secondary user verification challenge type
*/
constructor(suvType) {
super('challengeNeeded', suvType);
this.suvType = suvType;
}
}
此外,您还需要更新相应的执行响应以返回 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 error response handling
if(error instanceof ChallengeNeededError) {
result.challengeNeeded = {
type: error.suvType
};
}
}
})
);
}
}
}
最后,修改 updateDevice
以要求通过显式确认来开启或关闭洗衣机。
index.js
const updateDevice = async (execution,deviceId) => {
const {challenge,params,command} = execution; //Add secondary user challenge
let state, ref;
switch (command) {
case 'action.devices.commands.OnOff':
//Add secondary user verification 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 日志,查看第二层用户验证流程的每个步骤的详细回复。
8. 恭喜
恭喜!您通过 Modes
和 Toggles
特征扩展了智能家居 Action 的功能,并通过第二层用户身份验证确保了其执行安全。
了解详情
您可以实现以下想法以进行更深入的研究:
- 为您的设备添加本地执行功能。
- 使用其他第二层用户身份验证验证类型来修改设备状态。
- 更新
RunCycle
特征 QUERY 响应以进行动态更新。 - 探索此 GitHub 示例。