1. 准备工作
作为一名物联网 (IoT) 开发者,您可以构建云到云集成,让用户能够通过 Google Home 应用中的触控功能和 Google 助理的语音指令来控制设备。

云到云集成依靠 Home Graph 来提供住宅及其设备的环境数据,从而创建住宅逻辑图。借助这些环境数据,Google 助理能够根据用户在住宅中的位置更自然地理解用户请求。例如,Home Graph 可以存储客厅这个概念,而客厅中包含来自不同制造商的多种类型的设备(例如恒温器、灯、风扇和吸尘器)。

前提条件
- 创建云到云集成开发者指南
 
构建内容
在此 Codelab 中,您将发布一个用于管理虚拟智能洗衣机的云服务,然后构建一个云到云集成,并将其关联到 Google 助理。
学习内容
- 如何部署智能家居云服务
 - 如何将您的服务关联到 Google 助理
 - 如何将设备状态更改发布到 Google
 
所需条件
- 网络浏览器,例如 Google Chrome
 - 安装了 Google Home 应用的 iOS 或 Android 设备
 - Node.js 10.16 或更高版本
 - Google Cloud 结算账号
 
2. 开始使用
启用活动控件
若要使用 Google 助理,你必须与 Google 分享某些活动数据。Google 助理需要使用这些数据才能正常运行;然而,分享数据的要求并非专门针对该 SDK。如果你还没有 Google 账号,请创建一个 Google 账号,以便分享这些数据。您可以使用任何 Google 账号,不要求必须用您的开发者账号。
打开要与 Google 助理搭配使用的 Google 账号的活动控件页面。
确保已启用以下切换开关:
- 网络与应用活动记录 - 此外,请务必选中包括 Chrome 历史记录和使用 Google 服务的网站、应用和设备中的活动记录复选框。
 - 设备信息
 - 语音和音频活动记录
 
创建云到云集成项目
- 前往开发者控制台。
 - 点击创建项目,输入项目名称,然后点击创建项目。
 

选择云到云集成
在开发者控制台的项目首页中,选择云到云下的添加云到云集成。

安装 Firebase CLI
借助 Firebase 命令行界面 (CLI),您可以在本地提供 Web 应用,并将您的 Web 应用部署到 Firebase Hosting。
如需安装 CLI,请从终端运行以下 npm 命令:
npm install -g firebase-tools
如需验证 CLI 是否已正确安装,请运行以下命令:
firebase --version
运行以下命令,授权您的 Google 账号使用 Firebase CLI:
firebase login
3. 运行入门级应用
开发环境设置完毕后,你可以部署入门级项目以验证所有设置是否已配置正确。
获取源代码
点击以下链接,将此 Codelab 的示例下载到您的开发机器上:
您还可以从命令行克隆 GitHub 代码库:
git clone https://github.com/google-home/smarthome-washer.git
项目简介
入门级项目包含以下子目录:
public:一种前端界面,可轻松地控制和监控智能洗衣机的状态。functions:一种已全面实现的云服务,可使用 Cloud Functions for Firebase 和 Firebase Realtime Database 来管理智能洗衣机。
将 Firebase 添加到 Google Home 开发者控制台项目
方法 1:通过 Firebase 控制台
- 前往 Firebase。
 - 点击创建 Firebase 项目。

 - 在创建项目界面上,点击将 Firebase 添加到 Google Cloud 项目。

 - 在开始界面上,选择您刚刚在 Google Home Developer 控制台中创建的 Google Cloud 项目,然后点击继续。

 
方法 2:通过 Firebase CLI
firebase projects:addfirebase
选择您刚刚创建的 Google Home 开发者控制台项目,以添加 Firebase。
将 Firebase 添加到 Google Home 开发者控制台项目后,该项目将显示在 Firebase 控制台中。Firebase 项目的项目 ID 将与您的 Google Home 开发者控制台项目 ID 保持一致。

连接到 Firebase
前往 washer-start 目录,然后使用您的集成项目设置 Firebase CLI:
cd washer-start firebase use <project-id>
配置 Firebase 项目
初始化 Firebase 项目。
firebase init
选择 CLI 功能、Realtime Database 和 Functions 功能。
? Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed) >( ) Data Connect: Set up a Firebase Data Connect service ( ) Firestore: Configure security rules and indexes files for Firestore ( ) Genkit: Setup a new Genkit project with Firebase (*) Functions: Configure a Cloud Functions directory and its files ( ) App Hosting: Configure an apphosting.yaml file for App Hosting ( ) Hosting: Configure files for Firebase Hosting and (optionally) 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 (*) Realtime Database: Configure a security rules file for Realtime Database and (optionally) provision default instance ( ) Data Connect: Set up a Firebase Data Connect service ( ) Firestore: Configure security rules and indexes files for Firestore
这将初始化项目的必要 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 时,请选择否。
? File functions/.gitignore already exists. Overwrite? No
? Do you want to install dependencies with npm now? Yes
如果您不小心启用了 ESLint,可以通过以下两种方法将其停用:
- 使用 GUI,前往项目下的 
../functions文件夹,选择隐藏文件.eslintrc.js并将其删除。请勿将其与名称类似的.eslintrc.json混淆。 - 使用命令行:
cd functions rm .eslintrc.js
 
在 washer-start/firebase.json 文件中,使用以下代码完成代码:
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
    "headers": [{
      "source" : "**/*.@(js|html)",
      "headers" : [ {
        "key" : "Cache-Control",
        "value" : "max-age=0"
      } ]
    }],
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log",
        "*.local"
      ]
    }
  ]
}
部署到 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。此页面不会显示任何更改,但洗衣机的当前状态会存储在数据库中。
现在,您可以通过 Google Home 开发者控制台将您部署的云服务关联到 Google 助理。
配置开发者管理中心项目
在开发标签页中,为互动添加显示名称。此名称会显示在 Google Home 应用中。

在应用品牌推广下,上传应用图标的 png 文件,该文件的大小为 144 x 144 像素,名称为 。

如需启用账号关联,请使用以下账号关联设置:
客户 ID  | 
  | 
客户端密钥  | 
  | 
授权网址  | 
  | 
令牌网址  | 
  | 

在 Cloud fulfillment 网址 下,输入为智能家居 intent 提供执行方式的 Cloud Functions 函数的网址。
https://us-central1-<project-id>.cloudfunctions.net/smarthome

点击 Save 保存项目配置,然后点击 Next: Test 针对您的项目启用测试。

现在,您可以开始实现必要的网络钩子,以便将设备状态关联到 Google 助理。
4. 创建洗衣机
集成配置完毕后,您就可以添加设备并发送数据了。您的云服务需要处理以下 intent:
- 当 Google 助理想要了解用户关联了哪些设备时,就会发生 
SYNCintent。当用户关联账号时,系统会向您的服务发送此 intent。您应该在响应时提供所有用户设备及其功能的 JSON 载荷。 - 当 Google 助理想要了解设备的当前状态时,就会发生 
QUERYintent。您应该在响应时提供 JSON 载荷,其中包含所请求的每台设备的状态。 - 当 Google 助理想要代表用户控制设备时,就会发生 
EXECUTEintent。您应该在响应时提供 JSON 载荷,其中包含所请求的每台设备的执行状态。 - 当用户将账号与 Google 助理解除关联时,就会发生 
DISCONNECTintent。您应该停止向 Google 助理发送此用户设备的相关事件。 
您将在后面几个部分中更新之前部署的用于处理这些 intent 的函数。
更新 SYNC 响应
打开 functions/index.js,其中包含用于响应来自 Google 助理的请求的代码。
您需要通过返回设备元数据和功能来处理 SYNC intent。更新 onSync 数组中的 JSON,以便包含洗衣机的设备信息和推荐特征。
index.js
app.onSync((body) => {
  return {
    requestId: body.requestId,
    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',
        ],
        name: {
          defaultNames: ['My Washer'],
          name: 'Washer',
          nicknames: ['Washer'],
        },
        deviceInfo: {
          manufacturer: 'Acme Co',
          model: 'acme-washer',
          hwVersion: '1.0',
          swVersion: '1.0.1',
        },
        willReportState: true,
        attributes: {
          pausable: true,
        },
      }],
    },
  };
});
部署到 Firebase
使用 Firebase CLI 部署更新后的云执行方式:
firebase deploy --only functions
关联到 Google 助理
为了测试您的云到云集成,您需要将项目与 Google 账号相关联。这样一来,您就可以使用登录同一账号的 Google 助理界面和 Google Home 应用来进行测试。
- 在手机上打开 Google 助理设置。请注意,您登录的账号应该与控制台所用的账号相同。
 - 依次转到 Google 助理 > 设置 > 家居控制(位于“Google 助理”下方)。
 - 点击右上角的搜索图标。
 - 使用 [test] 前缀搜索您的测试应用,以找到特定的测试应用。
 - 选择此项目。然后,Google 助理会通过您的服务进行身份验证并发送 
SYNC请求,以要求您的服务提供用户设备列表。 
打开 Google Home 应用,然后验证您能否看到相应洗衣机设备。

5. 处理指令和查询
您的云服务可以向 Google 正确报告洗衣机设备后,您需要添加用于请求设备状态和发送命令的功能。
处理 QUERY intent
QUERY intent 包含一组设备。对于每种设备,您应该在响应时提供其当前状态。
在 functions/index.js 中,修改 QUERY 处理程序,以处理 intent 请求中包含的目标设备列表。
index.js
app.onQuery(async (body) => {
  const {requestId} = body;
  const payload = {
    devices: {},
  };
  const queryPromises = [];
  const intent = body.inputs[0];
  for (const device of intent.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
  await Promise.all(queryPromises);
  return {
    requestId: requestId,
    payload: payload,
  };
});
针对请求中包含的每台设备,返回 Realtime Database 中存储的当前状态。更新 queryFirebase 和 queryDevice 函数,以返回洗衣机的状态数据。
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,
  };
};
const queryDevice = async (deviceId) => {
  const data = await queryFirebase(deviceId);
  return {
    on: data.on,
    isPaused: data.isPaused,
    isRunning: data.isRunning,
    currentRunCycle: [{
      currentCycle: 'rinse',
      nextCycle: 'spin',
      lang: 'en',
    }],
    currentTotalRemainingTime: 1212,
    currentCycleRemainingTime: 301,
  };
};
处理 EXECUTE intent
EXECUTE intent 可处理用于更新设备状态的命令。响应会返回每个命令的状态(例如 SUCCESS、ERROR 或 PENDING)以及新的设备状态。
在 functions/index.js 中,修改 EXECUTE 处理程序,以处理需要更新的特征列表以及每个命令的目标设备集:
index.js
app.onExecute(async (body) => {
  const {requestId} = body;
  // Execution results are grouped by status
  const result = {
    ids: [],
    status: 'SUCCESS',
    states: {
      online: true,
    },
  };
  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(
            updateDevice(execution, device.id)
                .then((data) => {
                  result.ids.push(device.id);
                  Object.assign(result.states, data);
                })
                .catch(() => functions.logger.error('EXECUTE', device.id)));
      }
    }
  }
  await Promise.all(executePromises);
  return {
    requestId: requestId,
    payload: {
      commands: [result],
    },
  };
});
针对每个命令和目标设备,更新 Realtime Database 中与所请求的特征相对应的值。修改 updateDevice 函数,以更新相应的 Firebase 引用并返回更新后的设备状态。
index.js
const updateDevice = async (execution, deviceId) => {
  const {params, command} = execution;
  let state; let 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 = params.start
      ? {isRunning: true, isPaused: false}
      : {isRunning: false, isPaused: false};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
    case 'action.devices.commands.PauseUnpause':
      const data = await queryDevice(deviceId);
      state = (data.isPaused === false && data.isRunning === false)
        ? {isRunning: false, isPaused: false}
        : {isRunning: !params.pause, isPaused: params.pause};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
  }
  return ref.update(state)
      .then(() => state);
};
6. 测试集成
三个 intent 全部实现后,您可以测试集成是否能够控制洗衣机。
部署到 Firebase
使用 Firebase CLI 部署更新后的云执行方式:
firebase deploy --only functions
测试洗衣机
现在,如果您通过手机尝试下达以下任意一条语音指令,就会看到值发生变化:
“Ok Google,启动洗衣机。”
“Ok Google,让洗衣机暂停运行。”
“Ok Google,让洗衣机停止运行。”
您还可以通过提问来查询洗衣机的当前状态。
“Ok Google,我的洗衣机启动了吗?”
“Ok Google,我的洗衣机在运行吗?”
“Ok Google,我的洗衣机洗到哪一步了?”
您可以在 Firebase 控制台的函数部分中,查看函数下显示的日志中的这些查询和命令。如需详细了解 Firebase 日志,请参阅写入和查看日志。
您还可以前往 Google Cloud 控制台,依次选择 Logging > 日志浏览器,找到这些查询和命令。如需详细了解 Google Cloud 日志记录,请参阅使用 Cloud Logging 访问事件日志。
7. 向 Google 报告更新
您已将云服务与智能家居 intent 完全集成,让用户能够控制和查询设备的当前状态。不过,此实现仍缺少让您的服务能够主动向 Google 助理发送事件信息(例如,设备使用状态或状态的更改)的方法。
借助请求同步功能,您可以在用户添加或移除设备后,或在用户设备功能更改后触发新的同步请求。借助报告状态功能,在用户手动更改设备状态(例如,打开灯的开关)或使用其他服务更改状态后,您的云服务可以主动向 Home Graph 发送设备状态。
在此部分中,您将添加代码以从前端 Web 应用调用这些方法。
启用 HomeGraph API
借助 HomeGraph API,您可以在用户 Home Graph 中存储并查询设备及其状态。如需使用此 API,您必须先打开 Google Cloud 控制台,然后启用 HomeGraph API。
在 Google Cloud 控制台中,请务必选择与您的集成项目 <project-id>. 相匹配的项目。然后,在 HomeGraph API 的 API 库界面中,点击启用。

启用报告状态
对 Realtime Database 的写入操作会触发入门级项目中的 reportstate 函数。更新 functions/index.js 中的 reportstate 函数,以捕获写入数据库的数据,并通过状态报告将其发布到 Home Graph。
index.js
exports.reportstate = functions.database.ref('{deviceId}').onWrite(
    async (change, context) => {
      functions.logger.info('Firebase write event triggered Report State');
      const snapshot = change.after.val();
      const requestBody = {
        requestId: 'ff36a3cc', /* Any unique ID */
        agentUserId: USER_ID,
        payload: {
          devices: {
            states: {
              /* Report the current state of our washer */
              [context.params.deviceId]: {
                on: snapshot.OnOff.on,
                isPaused: snapshot.StartStop.isPaused,
                isRunning: snapshot.StartStop.isRunning,
              },
            },
          },
        },
      };
      const res = await homegraph.devices.reportStateAndNotification({
        requestBody,
      });
      functions.logger.info('Report state response:', res.status, res.data);
    });
启用请求同步
刷新前端网络界面的图标会触发入门级项目中的 requestsync 函数。在 functions/index.js 中实现 requestsync 函数,以调用 HomeGraph API。
index.js
exports.requestsync = functions.https.onRequest(async (request, response) => {
  response.set('Access-Control-Allow-Origin', '*');
  functions.logger.info(`Request SYNC for user ${USER_ID}`);
  try {
    const res = await homegraph.devices.requestSync({
      requestBody: {
        agentUserId: USER_ID,
      },
    });
    functions.logger.info('Request sync response:', res.status, res.data);
    response.json(res.data);
  } catch (err) {
    functions.logger.error(err);
    response.status(500).send(`Error requesting sync: ${err}`);
  }
});
部署到 Firebase
使用 Firebase CLI 部署更新后的代码:
firebase deploy --only functions
测试实现效果
点击网络界面中的刷新 
 按钮,并验证能否在 Firebase 控制台日志中看到同步请求。
接下来,调整前端网络界面中的洗衣机设备属性,然后点击更新。验证能否在 Firebase 控制台日志中看到向 Google 报告的状态更改。
8. 恭喜

恭喜!您已成功使用云到云集成将 Google 助理与设备云服务集成。
了解详情
您可以实现以下想法以进行更深入的研究:
您还可以详细了解如何测试和提交集成以供审核,包括用于向用户发布集成的认证流程。