将智能家居设备关联到 Google 助理

1. 准备工作

作为一名物联网 (IoT) 开发者,您可以构建智能家居 Action,让用户能够通过 Google Home 应用中的触控功能和 Google 助理的语音指令来控制设备。

79266e5f45e6ae20.gif

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

d009cef0f903d284.jpeg

前提条件

构建内容

在此 Codelab 中,您将发布一个用于管理虚拟智能洗衣机的云服务,然后构建一个智能家居 Action,并将其关联到 Google 助理。

学习内容

  • 如何部署智能家居云服务
  • 如何将您的服务关联到 Google 助理
  • 如何将设备状态更改发布到 Google

所需条件

2. 开始使用

启用活动控件

若要使用 Google 助理,你必须与 Google 分享某些活动数据。Google 助理需要使用这些数据才能正常运行;然而,分享数据的要求并非专门针对该 SDK。如果你还没有 Google 账号,请创建一个,以便分享这些数据。您可以使用任意 Google 账号,不一定要使用您的开发者账号。

打开要与 Google 助理搭配使用的 Google 账号的“活动控件”页面

确保已启用以下切换开关:

  • 网络与应用活动记录 - 此外,请务必选中包括 Chrome 历史记录和使用 Google 服务的网站、应用和设备中的活动记录复选框。
  • 设备信息
  • 语音和音频活动记录

创建 Actions 项目

  1. 前往 Actions on Google 开发者控制台
  2. 点击 New Project,输入项目名称,然后点击 CREATE PROJECT

3d6b68ca79afd54c

选择智能家居应用

在 Actions 控制台的“Overview”屏幕中,选择 Smart home

2fa4988f44f8914b

选择智能家居体验卡片,点击开始构建,然后你会进入项目控制台。

安装 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

转到 washer-start 目录,然后使用您的 Actions 项目设置 Firebase CLI:

cd washer-start
firebase use <project-id>

配置 Firebase 项目

初始化 Firebase 项目。

firebase init

选择 CLI 功能:Realtime DatabaseFunctions,以及包含 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.jspackage.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,有两种方法可以将其停用:

  1. 使用 GUI,转到项目下的 ../functions 文件夹,选择隐藏文件 .eslintrc.js 并将其删除。不要将其误认为名称相似的 .eslintrc.json
  2. 使用命令行:
    cd functions
    rm .eslintrc.js
    

为确保 Firebase 配置正确且完整,请将 firebase.json 文件从 washer-done 目录复制到 washer-start 目录,并覆盖 washer-start 中的文件。

washer-start 目录中执行以下操作:

cp -vp ../washer-done/firebase.json .

部署到 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 应用。您会看到以下界面:

5845443e94705557

此网络界面表示用于查看或修改设备状态的第三方平台。如需使用设备信息填充数据库,请点击 UPDATE。此页面不会显示任何更改,但洗衣机的当前状态会存储在数据库中。

现在,可以使用 Actions 控制台将你部署的云服务连接到 Google 助理了。

配置 Actions 控制台项目

Overview > Build your Action 下,选择 Add Action(s)。输入为智能家居 intent 提供执行方式的 Cloud Functions 函数的网址,然后点击 Save

https://us-central1-<project-id>.cloudfunctions.net/smarthome

9d7b223427f587ca.png

Develop > Invocation 标签页中,在 Display name 中为您的 Action 添加显示名,然后点击 Save。此名称会显示在 Google Home 应用中。

774d0c40c351c7da

a8c4673eb11d76ee.png

如需启用账号关联,请在左侧导航栏中依次选择 Develop > Account linking 选项。使用以下账号关联设置:

客户端 ID

ABC123

客户端密钥

DEF456

授权网址

https://us-central1-<project-id>.cloudfunctions.net/fakeauth

令牌网址

https://us-central1-<project-id>.cloudfunctions.net/faketoken

9730d20b90bcc038

点击 Save 保存您的账号关联配置,然后点击 Test 针对您的项目启用测试。

ee0547f05b5efd98.png

系统会将您重定向到 Simulator。如果您没有看到“Test now enabled”(立即启用测试),请点击重置测试以验证是否已启用测试。

d0495810dbadf059.png

现在,您可以开始实现必要的网络钩子,以便将设备状态关联到 Google 助理。

4. 创建洗衣机

Action 配置完毕后,您就可以添加设备并发送数据了。您的云服务需要处理以下 intent:

  • 当 Google 助理想要了解用户关联了哪些设备时,就会发生 SYNC intent。当用户关联账号时,系统会向您的服务发送此 intent。您应该在响应时提供所有用户设备及其功能的 JSON 载荷。
  • 当 Google 助理想要了解设备的当前状态时,就会发生 QUERY intent。您应该在响应时提供 JSON 载荷,其中包含所请求的每台设备的状态。
  • 当 Google 助理想要代表用户控制设备时,就会发生 EXECUTE intent。您应该在响应时提供 JSON 载荷,其中包含所请求的每台设备的执行状态。
  • 当用户解除其账号与 Google 助理的关联时,就会发生 DISCONNECT intent。您应该停止向 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

为了测试智能家居 Action,您需要将项目与 Google 账号相关联。这样一来,您就可以使用登录同一账号的 Google 助理界面和 Google Home 应用来进行测试。

  1. 在手机上打开 Google 助理设置。请注意,您登录的账号应该与控制台所用的账号相同。
  2. 依次转到 Google 助理 > 设置 > 家居控制(位于“Google 助理”下方)。
  3. 点击右上角的搜索图标
  4. 使用 [test] 前缀搜索您的测试应用,以查找您的特定测试应用。
  5. 选择此项目。然后,Google 助理会通过您的服务进行身份验证并发送 SYNC 请求,以要求您的服务提供用户设备列表。

打开 Google Home 应用,然后验证您能否看到相应洗衣机设备。

ae252220753726f6.png

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 中存储的当前状态。更新 queryFirebasequeryDevice 函数,以返回洗衣机的状态数据。

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 可处理用于更新设备状态的命令。响应会返回每个命令的状态(例如 SUCCESSERRORPENDING)以及新的设备状态。

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. 测试您的 Action

三个 intent 全部实现后,您可以测试 Action 是否能够控制洗衣机。

部署到 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 Console 中,请务必选择与您的 Actions <project-id>. 相匹配的项目。然后,在 HomeGraph API 的 API 库屏幕中,点击启用

ee198858a6eac112.png

启用报告状态

对 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

测试实现效果

点击网页界面中的刷新 ae8d3b25777a5e30.png 按钮,并验证是否在 Firebase 控制台日志中看到同步请求。

接下来,调整前端网络界面中的洗衣机设备属性,然后点击更新。验证能否在 Firebase 控制台日志中看到向 Google 报告的状态更改。

8. 恭喜

674c4f4392e98c1.png

恭喜!您已成功使用智能家居 Action 将 Google 助理与设备云服务集成。

了解详情

您可以实现以下想法以进行更深入的研究:

您还可以详细了解如何测试和提交 Action 以供审核,包括向用户发布 Action 的认证流程。