在 Android 设备上使用 Home API 创建自动化操作

1. 准备工作

这是使用 Google Home API 构建 Android 应用系列中的第二个 Codelab。在此 Codelab 中,我们将逐步介绍如何创建家庭自动化操作,并提供一些有关使用 API 的最佳实践技巧。如果您尚未完成第一个 Codelab,即使用 Android 上的 Home API 构建移动应用,建议您先完成该 Codelab,然后再开始学习本 Codelab。

Google Home API 提供了一组库,供 Android 开发者在 Google Home 生态系统中控制智能家居设备。借助这些新 API,开发者将能够为智能家居设置自动化操作,以便根据预定义的条件控制设备功能。Google 还提供了一个 Discovery API,可让您查询设备以了解它们支持哪些属性和命令。

前提条件

学习内容

  • 如何使用 Home API 为智能家居设备创建自动化操作。
  • 如何使用 Discovery API 探索支持的设备功能。
  • 如何在通过 Home API 构建应用时采用最佳实践。

2. 设置项目

下图展示了 Home API 应用的架构:

Android 应用的 Home API 架构

  • 应用代码:开发者用于构建应用的用户界面以及与 Home APIs SDK 交互的逻辑的核心代码。
  • Home APIs SDK:Google 提供的 Home APIs SDK 可与 GMSCore 中的 Home APIs 服务搭配使用,以控制智能家居设备。开发者通过将 Home API 与 Home API SDK 捆绑在一起,构建可与 Home API 搭配使用的应用。
  • Android 上的 GMSCore:GMSCore 也称为 Google Play 服务,是一个 Google 平台,可提供核心系统服务,从而在所有经过认证的 Android 设备上实现多种关键功能。Google Play 服务的首页模块包含与 Home API 交互的服务。

在此 Codelab 中,我们将以 在 Android 上使用 Home API 构建移动应用中所述的内容为基础。

确保您已在账号中设置并运行至少两个受支持的设备。由于我们将在本 Codelab 中设置自动化操作(设备状态发生变化时触发另一设备上的操作),因此您需要两部设备才能看到结果。

获取示例应用

示例应用的源代码可在 GitHub 上的 google-home/google-home-api-sample-app-android 代码库中找到。

此 Codelab 使用的是示例应用的 codelab-branch-2 分支中的示例。

前往您要保存项目的位置,然后克隆 codelab-branch-2 分支:

$ git clone -b codelab-branch-2 https://github.com/google-home/google-home-api-sample-app-android.git

请注意,此分支与在 Android 上使用 Home API 构建移动应用中所用的分支不同。此代码库分支基于第一个 Codelab 的结束位置构建。这次,示例将引导您了解如何创建自动化操作。如果您已完成之前的 Codelab,并且能够让所有功能正常运行,则可以选择使用同一 Android Studio 项目来完成此 Codelab,而不是使用 codelab-branch-2

编译源代码并使其可在移动设备上运行后,请继续执行下一部分。

3. 了解自动化功能

自动化是一组“如果这样,就那样”的语句,可根据所选因素以自动方式控制设备状态。开发者可以使用自动化功能在其 API 中构建高级互动功能。

自动化操作由三种不同类型的组件(称为节点)组成:启动方式、操作和条件。这些节点可协同工作,使用智能家居设备自动执行操作。通常,它们会按以下顺序进行评估:

  1. 启动方式 - 定义用于激活自动化操作的初始条件,例如特性的值发生变化。自动化操作必须具有启动方式
  2. 条件 - 触发自动化操作后要评估的任何其他限制。条件中的表达式必须计算为 true,自动化操作才能执行。
  3. 操作 - 在满足所有条件时执行的命令或状态更新。

例如,您可以设置一个自动化操作,当开关切换时,调暗房间内的灯光,同时打开房间内的电视。在此示例中:

  • 启动器 - 房间中的开关已切换。
  • 条件 - 电视的 OnOff 状态评估结果为“开启”。
  • 操作 - 调暗开关所在房间的灯光。

这些节点由自动化引擎以串行或并行方式进行评估。

image5.png

顺序流包含按顺序执行的节点。通常,这些是启动方式、条件和操作。

image6.png

并行流程可能包含多个同时执行的操作节点,例如同时打开多盏灯。并行流程中的节点只有在并行流程的所有分支都完成时才会执行。

自动化架构中还有其他类型的节点。如需详细了解这些节点,请参阅 Home API 开发者指南的节点部分。此外,开发者还可以组合不同类型的节点来创建复杂的自动化流程,例如:

image13.png

开发者使用专门为 Google Home 自动化创建的领域特定语言 (DSL) 向自动化引擎提供这些节点。

探索自动化 DSL

领域特定语言 (DSL) 是一种用于以代码形式捕获系统行为的语言。编译器会生成序列化为协议缓冲区 JSON 的数据类,并用于调用 Google 的自动化服务。

DSL 会查找以下架构:

automation {
name = "AutomationName"
  description = "An example automation description."
  isActive = true
    sequential {
    val onOffTrait = starter<_>(device1, OnOffLightDevice, OnOff)
    condition() { expression = onOffTrait.onOff equals true }
    action(device2, OnOffLightDevice) { command(OnOff.on()) }
  }
}

上例中的自动化操作会同步两个灯泡。当 device1OnOff 状态更改为 On (onOffTrait.onOff equals true) 时,device2OnOff 状态会更改为 On (command(OnOff.on())。

使用自动化功能时,请注意存在资源限制

自动化是一项非常实用的工具,可用于在智能家居中创建自动化功能。在最基本的用例中,您可以明确编写自动化代码,以使用特定设备和特征。但更实用的使用情形是,应用允许用户配置自动化操作的设备、命令和参数。下一部分将介绍如何创建自动化编辑器,让用户能够实现上述目标。

4. 构建自动化编辑器

在示例应用中,我们将创建一个自动化编辑器,用户可以使用该编辑器选择设备、要使用的功能(操作),以及如何使用启动器触发自动化操作。

img11-01.png img11-02.png img11-03.png img11-04.png

设置启动方式

自动化操作启动方式是自动化操作的入口点。启动器会在发生指定事件时触发自动化操作。在示例应用中,我们使用 StarterViewModel 类(位于 StarterViewModel.kt 源文件中)捕获自动化启动器,并使用 StarterView (StarterView.kt) 显示编辑器视图。

初始节点需要以下元素:

  • 设备
  • 特征
  • 操作

设备和特征可以从 Devices API 返回的对象中选择。每种受支持设备的命令和参数都比较复杂,需要单独处理。

应用定义了一份预设的操作列表:

   // List of operations available when creating automation starters:
enum class Operation {
  EQUALS,
  NOT_EQUALS,
  GREATER_THAN,
  GREATER_THAN_OR_EQUALS,
  LESS_THAN,
  LESS_THAN_OR_EQUALS
    }

然后,针对每个受支持的特征,跟踪受支持的操作:

// List of operations available when comparing booleans:
 object BooleanOperations : Operations(listOf(
     Operation.EQUALS,
     Operation.NOT_EQUALS
 ))
// List of operations available when comparing values:
object LevelOperations : Operations(listOf(
    Operation.GREATER_THAN,
    Operation.GREATER_THAN_OR_EQUALS,
    Operation.LESS_THAN,
    Operation.LESS_THAN_OR_EQUALS
))

同样,示例应用会跟踪可分配给特征的值:

enum class OnOffValue {
   On,
   Off,
}
enum class ThermostatValue {
  Heat,
  Cool,
  Off,
}

并跟踪应用定义的值与 API 定义的值之间的映射:

val valuesOnOff: Map<OnOffValue, Boolean> = mapOf(
  OnOffValue.On to true,
  OnOffValue.Off to false,
)
val valuesThermostat: Map<ThermostatValue, ThermostatTrait.SystemModeEnum> = mapOf(
  ThermostatValue.Heat to ThermostatTrait.SystemModeEnum.Heat,
  ThermostatValue.Cool to ThermostatTrait.SystemModeEnum.Cool,
  ThermostatValue.Off to ThermostatTrait.SystemModeEnum.Off,
)

然后,应用会显示一组视图元素,用户可以使用这些元素来选择所需的字段。

StarterView.kt 文件中取消注释步骤 4.1.1,以渲染所有初始设备并在 DropdownMenu 中实现点击回调:

val deviceVMs: List<DeviceViewModel> = structureVM.deviceVMs.collectAsState().value
...
DropdownMenu(expanded = expandedDeviceSelection, onDismissRequest = { expandedDeviceSelection = false }) {
// TODO: 4.1.1 - Starter device selection dropdown
// for (deviceVM in deviceVMs) {
//     DropdownMenuItem(
//         text = { Text(deviceVM.name) },
//         onClick = {
//             scope.launch {
//                 starterDeviceVM.value = deviceVM
//                 starterType.value = deviceVM.type.value
//                 starterTrait.value = null
//                 starterOperation.value = null
//             }
//             expandedDeviceSelection = false
//         }
//     )
// }
}

StarterView.kt 文件中取消注释步骤 4.1.2,以呈现初始设备的全部特征,并在 DropdownMenu 中实现点击回调:

// Selected starter attributes for StarterView on screen:
val starterDeviceVM: MutableState<DeviceViewModel?> = remember {
mutableStateOf(starterVM.deviceVM.value) }
...
DropdownMenu(expanded = expandedTraitSelection, onDismissRequest = { expandedTraitSelection = false }) {
// TODO: 4.1.2 - Starter device traits selection dropdown
// val deviceTraits = starterDeviceVM.value?.traits?.collectAsState()?.value!!
// for (trait in deviceTraits) {
//     DropdownMenuItem(
//         text = { Text(trait.factory.toString()) },
//         onClick = {
//             scope.launch {
//                 starterTrait.value = trait.factory
//                 starterOperation.value = null
//             }
//             expandedTraitSelection = false
//         }
//     )
}
}

取消 StarterView.kt 文件中第 4.1.3 步的注释,以呈现所选特征的所有操作,并在 DropdownMenu 中实现点击回调:

val starterOperation: MutableState<StarterViewModel.Operation?> = remember {
  mutableStateOf(starterVM.operation.value) }
  ...
  DropdownMenu(expanded = expandedOperationSelection, onDismissRequest = { expandedOperationSelection = false }) {
    // ...
    if (!StarterViewModel.starterOperations.containsKey(starterTrait.value))
    return@DropdownMenu
    // TODO: 4.1.3 - Starter device trait operations selection dropdown
      // val operations: List<StarterViewModel.Operation> = StarterViewModel.starterOperations.get(starterTrait.value ?: OnOff)?.operations!!
    //  for (operation in operations) {
    //      DropdownMenuItem(
    //          text = { Text(operation.toString()) },
    //          onClick = {
    //              scope.launch {
    //                  starterOperation.value = operation
    //              }
    //              expandedOperationSelection = false
    //          }
    //      )
    //  }
}

StarterView.kt 文件中取消注释步骤 4.1.4,以呈现所选特征的所有值,并在 DropdownMenu 中实现点击回调:

when (starterTrait.value) {
  OnOff -> {
        ...
    DropdownMenu(expanded = expandedBooleanSelection, onDismissRequest = { expandedBooleanSelection = false }) {
// TODO: 4.1.4 - Starter device trait values selection dropdown
//             for (value in StarterViewModel.valuesOnOff.keys) {
//                 DropdownMenuItem(
//                     text = { Text(value.toString()) },
//                     onClick = {
//                         scope.launch {
//                             starterValueOnOff.value = StarterViewModel.valuesOnOff.get(value)
//                         }
//                         expandedBooleanSelection = false
//                     }
//                 )
//             }
             }
              ...
          }
           LevelControl -> {
              ...
      }
   }

取消注释 StarterView.kt 文件中的步骤 4.1.5,以将所有启动器 ViewModel 变量存储到草稿自动化操作的启动器 ViewModel (draftVM.starterVMs) 中。

val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save starter button:
Button(
enabled = isOptionsSelected && isValueProvided,
onClick = {
  scope.launch {
  // TODO: 4.1.5 - store all starter ViewModel variables into draft ViewModel
  // starterVM.deviceVM.emit(starterDeviceVM.value)
  // starterVM.trait.emit(starterTrait.value)
  // starterVM.operation.emit(starterOperation.value)
  // starterVM.valueOnOff.emit(starterValueOnOff.value!!)
  // starterVM.valueLevel.emit(starterValueLevel.value!!)
  // starterVM.valueBooleanState.emit(starterValueBooleanState.value!!)
  // starterVM.valueOccupancy.emit(starterValueOccupancy.value!!)
  // starterVM.valueThermostat.emit(starterValueThermostat.value!!)
  //
  // draftVM.starterVMs.value.add(starterVM)
  // draftVM.selectedStarterVM.emit(null)
  }
})
{ Text(stringResource(R.string.starter_button_create)) }

运行应用并选择新的自动化操作和启动器后,应显示如下视图:

79beb3b581ec71ec.png

示例应用仅支持基于设备特征的启动器。

设置操作

自动化操作反映了自动化的核心用途,即它如何在现实世界中产生变化。在示例应用中,我们使用 ActionViewModel 类捕获自动化操作,并使用 ActionView 类显示编辑器视图。

示例应用使用以下 Home API 实体来定义自动化操作节点:

  • 设备
  • 特征
  • 命令
  • 值(可选)

每项设备命令操作都会使用一个命令,但有些命令还需要关联的参数值,例如 MoveToLevel() 和目标百分比。

设备和特征可以从 Devices API 返回的对象中选择。

应用定义了预定义的命令列表:

   // List of operations available when creating automation starters:
enum class Action {
  ON,
  OFF,
  MOVE_TO_LEVEL,
  MODE_HEAT,
  MODE_COOL,
  MODE_OFF,
}

该应用会跟踪每个受支持的特征所支持的操作:

 // List of operations available when comparing booleans:
object OnOffActions : Actions(listOf(
    Action.ON,
    Action.OFF,
))
// List of operations available when comparing booleans:
object LevelActions : Actions(listOf(
    Action.MOVE_TO_LEVEL
))
// List of operations available when comparing booleans:
object ThermostatActions : Actions(listOf(
    Action.MODE_HEAT,
    Action.MODE_COOL,
    Action.MODE_OFF,
))
// Map traits and the comparison operations they support:
val actionActions: Map<TraitFactory<out Trait>, Actions> = mapOf(
    OnOff to OnOffActions,
    LevelControl to LevelActions,
 // BooleanState - No Actions
 // OccupancySensing - No Actions
    Thermostat to ThermostatActions,
)

对于接受一个或多个参数的命令,还有一个变量:

   val valueLevel: MutableStateFlow<UByte?>

该 API 会显示一组视图元素,用户可以使用这些元素来选择所需的字段。

取消 ActionView.kt 文件中步骤 4.2.1 的注释,以渲染所有操作设备,并在 DropdownMenu 中实现点击回调以设置 actionDeviceVM

val deviceVMs = structureVM.deviceVMs.collectAsState().value
...
DropdownMenu(expanded = expandedDeviceSelection, onDismissRequest = { expandedDeviceSelection = false }) {
// TODO: 4.2.1 - Action device selection dropdown
// for (deviceVM in deviceVMs) {
//     DropdownMenuItem(
//         text = { Text(deviceVM.name) },
//         onClick = {
//             scope.launch {
//                 actionDeviceVM.value = deviceVM
//                 actionTrait.value = null
//                 actionAction.value = null
//             }
//             expandedDeviceSelection = false
//         }
//     )
// }
}

取消 ActionView.kt 文件中步骤 4.2.2 的注释,以呈现 actionDeviceVM 的所有特征,并在 DropdownMenu 中实现点击回调以设置 actionTrait,表示命令所属的特征。

val actionDeviceVM: MutableState<DeviceViewModel?> = remember {
mutableStateOf(actionVM.deviceVM.value) }
...
DropdownMenu(expanded = expandedTraitSelection, onDismissRequest = { expandedTraitSelection = false }) {
// TODO: 4.2.2 - Action device traits selection dropdown
// val deviceTraits: List<Trait> = actionDeviceVM.value?.traits?.collectAsState()?.value!!
// for (trait in deviceTraits) {
//     DropdownMenuItem(
//         text = { Text(trait.factory.toString()) },
//         onClick = {
//             scope.launch {
//                 actionTrait.value = trait
//                 actionAction.value = null
//             }
//             expandedTraitSelection = false
//         }
//     )
// }
}

取消 ActionView.kt 文件中第 4.2.3 步的注释,以呈现 actionTrait 的所有可用操作,并在 DropdownMenu 中实现点击回调以设置 actionAction,该变量表示所选的自动化操作。

DropdownMenu(expanded = expandedActionSelection, onDismissRequest = { expandedActionSelection = false }) {
// ...
if (!ActionViewModel.actionActions.containsKey(actionTrait.value?.factory))
return@DropdownMenu
// TODO: 4.2.3 - Action device trait actions (commands) selection dropdown
// val actions: List<ActionViewModel.Action> = ActionViewModel.actionActions.get(actionTrait.value?.factory)?.actions!!
// for (action in actions) {
//     DropdownMenuItem(
//         text = { Text(action.toString()) },
//         onClick = {
//             scope.launch {
//                 actionAction.value = action
//             }
//             expandedActionSelection = false
//         }
//     )
// }
}

ActionView.kt 文件中取消对步骤 4.2.4 的注释,以呈现特征操作(命令)的可用值,并在值更改回调中将该值存储到 actionValueLevel 中:

when (actionTrait.value?.factory) {
LevelControl -> {
// TODO: 4.2.4 - Action device trait action(command) values selection widget
// Column (Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth()) {
//   Text(stringResource(R.string.action_title_value), fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
//  }
//
//  Box (Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
//      LevelSlider(value = actionValueLevel.value?.toFloat()!!, low = 0f, high = 254f, steps = 0,
//          modifier = Modifier.padding(top = 16.dp),
//          onValueChange = { value : Float -> actionValueLevel.value = value.toUInt().toUByte() }
//          isEnabled = true
//      )
//  }
...
}

取消注释 ActionView.kt 文件中的第 4.2.5 步,以将所有操作 ViewModel 的变量存储在草稿自动化操作 ViewModel (draftVM.actionVMs) 中:

val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save action button:
Button(
  enabled = isOptionsSelected,
  onClick = {
  scope.launch {
  // TODO: 4.2.5 - store all action ViewModel variables into draft ViewModel
  // actionVM.deviceVM.emit(actionDeviceVM.value)
  // actionVM.trait.emit(actionTrait.value)
  // actionVM.action.emit(actionAction.value)
  // actionVM.valueLevel.emit(actionValueLevel.value)
  //
  // draftVM.actionVMs.value.add(actionVM)
  // draftVM.selectedActionVM.emit(null)
  }
})
{ Text(stringResource(R.string.action_button_create)) }

运行应用并选择新的自动化操作和操作后,应会显示如下视图:

6efa3c7cafd3e595.png

在示例应用中,我们仅支持基于设备特征的操作。

呈现自动化草稿

DraftViewModel 完成后,可以由 HomeAppView.kt 进行渲染:

fun HomeAppView (homeAppVM: HomeAppViewModel) {
  ...
  // If a draft automation is selected, show the draft editor:
  if (selectedDraftVM != null) {
    DraftView(homeAppVM)
  }
  ...
}

DraftView.kt 中:

fun DraftView (homeAppVM: HomeAppViewModel) {
   val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
    ...
// Draft Starters:
   DraftStarterList(draftVM)
// Draft Actions:
   DraftActionList(draftVM)
}

创建自动化操作

现在,您已了解如何创建启动器和操作,接下来可以创建自动化草稿并将其发送到 Automation API。该 API 具有 createAutomation() 函数,该函数将自动化草稿作为实参,并返回新的自动化实例。

草稿自动化准备工作在示例应用中的 DraftViewModel 类中进行。查看 getDraftAutomation() 函数,详细了解我们如何使用上一部分中的启动器和操作变量来构建自动化草稿。

取消注释 DraftViewModel.kt 文件中的步骤 4.4.1,以创建在启动器特征为 OnOff 时创建自动化图所需的“选择”表达式:

val starterVMs: List<StarterViewModel> = starterVMs.value
val actionVMs: List<ActionViewModel> = actionVMs.value
    ...
fun getDraftAutomation() : DraftAutomation {
    ...
  val starterVMs: List<StarterViewModel> = starterVMs.value
    ...
  return automation {
    this.name = name
    this.description = description
    this.isActive = true
    // The sequential block wrapping all nodes:
    sequential {
    // The select block wrapping all starters:
      select {
    // Iterate through the selected starters:
        for (starterVM in starterVMs) {
        // The sequential block for each starter (should wrap the Starter Expression!)
          sequential {
              ...
              val starterTrait: TraitFactory<out Trait> = starterVM.trait.value!!
              ...
              when (starterTrait) {
                  OnOff -> {
        // TODO: 4.4.1 - Set starter expressions according to trait type
        //   val onOffValue: Boolean = starterVM.valueOnOff.value
        //   val onOffExpression: TypedExpression<out OnOff> =
        //       starterExpression as TypedExpression<out OnOff>
        //   when (starterOperation) {
        //       StarterViewModel.Operation.EQUALS ->
        //           condition { expression = onOffExpression.onOff equals onOffValue }
        //       StarterViewModel.Operation.NOT_EQUALS ->
        //           condition { expression = onOffExpression.onOff notEquals onOffValue }
        //       else -> { MainActivity.showError(this, "Unexpected operation for OnOf
        //   }
        }
   LevelControl -> {
     ...
// Function to allow manual execution of the automation:
manualStarter()
     ...
}

取消 DraftViewModel.kt 文件中步骤 4.4.2 的注释,以创建在所选操作特征为 LevelControl 且所选操作为 MOVE_TO_LEVEL 时创建自动化图所需的并行表达式:

val starterVMs: List<StarterViewModel> = starterVMs.value
val actionVMs: List<ActionViewModel> = actionVMs.value
    ...
fun getDraftAutomation() : DraftAutomation {
      ...
  return automation {
    this.name = name
    this.description = description
    this.isActive = true
    // The sequential block wrapping all nodes:
    sequential {
          ...
    // Parallel block wrapping all actions:
      parallel {
        // Iterate through the selected actions:
        for (actionVM in actionVMs) {
          val actionDeviceVM: DeviceViewModel = actionVM.deviceVM.value!!
        // Action Expression that the DSL will check for:
          action(actionDeviceVM.device, actionDeviceVM.type.value.factory) {
            val actionCommand: Command = when (actionVM.action.value) {
                  ActionViewModel.Action.ON -> { OnOff.on() }
                  ActionViewModel.Action.OFF -> { OnOff.off() }
    // TODO: 4.4.2 - Set starter expressions according to trait type
    // ActionViewModel.Action.MOVE_TO_LEVEL -> {
    //     LevelControl.moveToLevelWithOnOff(
    //         actionVM.valueLevel.value!!,
    //         0u,
    //         LevelControlTrait.OptionsBitmap(),
    //         LevelControlTrait.OptionsBitmap()
    //     )
    // }
      ActionViewModel.Action.MODE_HEAT -> { SimplifiedThermostat
      .setSystemMode(SimplifiedThermostatTrait.SystemModeEnum.Heat) }
          ...
}

完成自动化的最后一步是实现 getDraftAutomation 函数以创建 AutomationDraft.

取消注释 HomeAppViewModel.kt 文件中的第 4.4.3 步,以通过调用 Home API 和处理异常来创建自动化操作:

fun createAutomation(isPending: MutableState<Boolean>) {
  viewModelScope.launch {
    val structure : Structure = selectedStructureVM.value?.structure!!
    val draft : DraftAutomation = selectedDraftVM.value?.getDraftAutomation()!!
    isPending.value = true
    // TODO: 4.4.3 - Call the Home API to create automation and handle exceptions
    // // Call Automation API to create an automation from a draft:
    // try {
    //     structure.createAutomation(draft)
    // }
    // catch (e: Exception) {
    //     MainActivity.showError(this, e.toString())
    //     isPending.value = false
    //     return@launch
    // }
    // Scrap the draft and automation candidates used in the process:
    selectedCandidateVMs.emit(null)
    selectedDraftVM.emit(null)
    isPending.value = false
  }
}

现在运行应用,看看设备上的变化!

选择启动方式和操作后,您就可以创建自动化操作了:

ec551405f8b07b8e.png

确保为自动化操作指定一个唯一的名称,然后点按创建自动化操作按钮,该按钮应会调用 API 并将您带回到自动化操作列表视图,其中包含您的自动化操作:

8eebc32cd3755618.png

点按刚刚创建的自动化操作,查看 API 如何返回该操作。

931dba7c325d6ef7.png

请注意,该 API 会返回一个值,用于指明自动化是否有效且当前处于活跃状态。创建的自动化操作在服务器端解析时可能无法通过验证。如果自动化解析未能通过验证,则 isValid 会设置为 false,表示自动化无效且处于非活跃状态。如果自动化无效,请查看 automation.validationIssues 字段了解详情。

确保自动化操作已设置为有效且处于启用状态,然后即可试用该自动化操作。

试用自动化操作

自动化可以通过两种方式执行:

  1. 使用启动方式事件。如果条件匹配,系统会触发您在自动化操作中设置的操作。
  2. 通过手动执行 API 调用。

如果自动化草稿在自动化草稿 DSL 块中定义了 manualStarter(),自动化引擎将支持手动执行该自动化。示例应用的代码示例中已包含此内容。

由于您仍处于移动设备上的自动化视图界面,请点按手动执行按钮。这应该会调用 automation.execute(),后者会在您设置自动化时选择的设备上运行操作命令。

通过使用 API 手动执行操作命令来验证该命令后,现在可以看看它是否也能使用您定义的启动器来执行。

前往“设备”标签页,选择操作设备和特征,然后将其设置为不同的值(例如,将 light2LevelControl [亮度] 设置为 50%,如以下屏幕截图所示):

d0357ec71325d1a8.png

现在,我们将尝试使用启动设备触发自动化操作。选择您在创建自动化操作时选择的启动设备。切换您选择的特征(例如,将 starter outlet1OnOff 设置为 On):

230c78cd71c95564.png

您会看到,这也会执行自动化操作,并将操作设备 light2LevelControl 特征设置为原始值 100%:

1f00292128bde1c2.png

恭喜,您已成功使用 Home API 创建自动化操作!

如需详细了解 Automation API,请参阅 Android Automation API

5. 发现功能

Home API 包含一个名为 Discovery API 的专用 API,开发者可以使用该 API 查询指定设备支持哪些可实现自动化的特征。示例应用提供了一个示例,您可以使用此 API 发现哪些命令可用。

探索命令

在本部分中,我们将讨论如何发现受支持的 CommandCandidates,以及如何基于发现的候选节点创建自动化。

在示例应用中,我们调用 device.candidates() 来获取候选列表,该列表可能包含 CommandCandidateEventCandidateTraitAttributesCandidate 的实例。

前往 HomeAppViewModel.kt 文件,然后取消对步骤 5.1.1 的注释,以检索候选列表并使用 Candidate 类型进行过滤:

   fun showCandidates() {

   ...
// TODO: 5.1.1 - Retrieve automation candidates, filtering to include CommandCandidate types only
// // Retrieve a set of initial automation candidates from the device:
// val candidates: Set<NodeCandidate> = deviceVM.device.candidates().first()
//
// for (candidate in candidates) {
//     // Check whether the candidate trait is supported:
//     if(candidate.trait !in HomeApp.supportedTraits)
//         continue
//     // Check whether the candidate type is supported:
//     when (candidate) {
//         // Command candidate type:
//         is CommandCandidate -> {
//             // Check whether the command candidate has a supported command:
//             if (candidate.commandDescriptor !in ActionViewModel.commandMap)
//                 continue
//         }
//         // Other candidate types are currently unsupported:
//         else -> { continue }
//     }
//
//     candidateVMList.add(CandidateViewModel(candidate, deviceVM))
// }
...
           // Store the ViewModels:
selectedCandidateVMs.emit(candidateVMList)
}

请注意,此示例会过滤 CommandCandidate. API 返回的候选对象属于不同类型。示例应用支持 CommandCandidate。取消注释 ActionViewModel.kt 中定义的 commandMap 中的第 5.1.2 步,以设置以下受支持的特征:

    // Map of supported commands from Discovery API:
val commandMap: Map<CommandDescriptor, Action> = mapOf(
    // TODO: 5.1.2 - Set current supported commands
    // OnOffTrait.OnCommand to Action.ON,
    // OnOffTrait.OffCommand to Action.OFF,
    // LevelControlTrait.MoveToLevelWithOnOffCommand to Action.MOVE_TO_LEVEL
)

现在,我们已经能够调用 Discovery API 并过滤我们在示例应用中支持的结果,接下来我们将讨论如何将其集成到编辑器中。

8a2f0e8940f7056a.png

如需详细了解 Discovery API,请访问在 Android 上利用设备发现功能

集成编辑器

使用已发现操作的最常见方式是将其呈现给最终用户以供选择。在用户选择草稿自动化字段之前,我们可以向其显示已发现的操作列表,并根据用户选择的值预先填充自动化草稿中的操作节点。

CandidatesView.kt 文件包含用于显示发现的候选对象的视图类。取消注释步骤 5.2.1 以启用 CandidateListItem.clickable{} 函数,该函数将 homeAppVM.selectedDraftVM 设置为 candidateVM

fun CandidateListItem (candidateVM: CandidateViewModel, homeAppVM: HomeAppViewModel) {
    val scope: CoroutineScope = rememberCoroutineScope()
    Box (Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
        Column (Modifier.fillMaxWidth().clickable {
        // TODO: 5.2.1 - Set the selectedDraftVM to the selected candidate
        // scope.launch { homeAppVM.selectedDraftVM.emit(DraftViewModel(candidateVM)) }
        }) {
            ...
        }
    }
}

HomeAppView.kt 中的第 4.3 步类似,当设置 selectedDraftVM 时,它会呈现 DraftView(...) in DraftView.kt:

fun HomeAppView (homeAppVM: HomeAppViewModel) {
   ...
  val selectedDraftVM: DraftViewModel? by homeAppVM.selectedDraftVM.collectAsState()
...
  // If a draft automation is selected, show the draft editor:
  if (selectedDraftVM != null) {
  DraftView(homeAppVM)
  }
   ...
}

再次尝试,点按上一部分中显示的 light2 - MOVE_TO_LEVEL,系统会提示您根据候选命令创建新的自动化操作:

15e67763a9241000.png

现在,您已熟悉如何在示例应用中创建自动化操作,接下来可以在自己的应用中集成自动化操作。

6. 高级自动化示例

在结束之前,我们将讨论一些额外的自动化 DSL 示例。这些示例展示了您可以通过 API 实现的一些高级功能。

作为启动器的每日时间

除了设备特征之外,Google Home API 还提供基于结构的特征,例如 Time。您可以创建具有基于时间的启动器的自动化操作,如下所示:

automation {
  name = "AutomationName"
  description = "An example automation description."
  isActive = true
  description = "Do ... actions when time is up."
  sequential {
    // starter
    val starter = starter<_>(structure, Time.ScheduledTimeEvent) {
      parameter(
        Time.ScheduledTimeEvent.clockTime(
          LocalTime.of(hour, min, sec, 0)
        )
      )
    }
        // action
  ...
  }
}

将 Google 助理广播作为操作

AssistantBroadcast 特征既可以作为 SpeakerDevice 中的设备级特征(如果音箱支持),也可以作为结构级特征(因为 Google 音箱和 Android 移动设备可以播放助理广播)。例如:

automation {
  name = "AutomationName"
  description = "An example automation description."
  isActive = true
  description = "Broadcast in Speaker when ..."
  sequential {
    // starter
      ...
    // action
    action(structure) {
      command(
      AssistantBroadcast.broadcast("Time is up!!")
      )
    }
  }
}

使用 DelayForsuppressFor

自动化 API 还提供了一些高级运算符,例如用于延迟命令的 delayFor 和用于在指定时间范围内禁止同一事件触发自动化的 suppressFor。以下是一些使用这些运算符的示例:

sequential {
  val starterNode = starter<_>(device, OccupancySensorDevice, MotionDetection)
  // only proceed if there is currently motion taking place
  condition { starterNode.motionDetectionEventInProgress equals true }
   // ignore the starter for one minute after it was last triggered
    suppressFor(Duration.ofMinutes(1))
  
    // make announcements three seconds apart
    action(device, SpeakerDevice) {
      command(AssistantBroadcast.broadcast("Intruder detected!"))
    }
    delayFor(Duration.ofSeconds(3))
    action(device, SpeakerDevice) {
    command(AssistantBroadcast.broadcast("Intruder detected!"))
  }
    ...
}

在启动器中使用 AreaPresenceState

AreaPresenceState 是一种结构级特征,用于检测是否有人在家。

例如,以下示例演示了在晚上 10 点后有人在家时自动锁门:

automation {
  name = "Lock the doors when someone is home after 10pm"
  description = "1 starter, 2 actions"
  sequential {
    val unused =
      starter(structure, event = Time.ScheduledTimeEvent) {
        parameter(Time.ScheduledTimeEvent.clockTime(LocalTime.of(22, 0, 0, 0)))
      }
    val stateReaderNode = stateReader<_>(structure, AreaPresenceState)
    condition {
      expression =
        stateReaderNode.presenceState equals
          AreaPresenceStateTrait.PresenceState.PresenceStateOccupied
    }
    action(structure) { command(AssistantBroadcast.broadcast("Locks are being applied")) }
    for (lockDevice in lockDevices) {
      action(lockDevice, DoorLockDevice) {
        command(Command(DoorLock, DoorLockTrait.LockDoorCommand.requestId.toString(), mapOf()))
      }
    }
  }

现在,您已经熟悉了这些高级自动化功能,快去创建出色的应用吧!

7. 恭喜!

恭喜!您已成功完成使用 Google Home API 开发 Android 应用的第二部分。在本 Codelab 中,您探索了 Automation 和 Discovery API。

我们希望您能尽情享受使用 Home API 构建应用的过程,在 Google Home 生态系统中以富有创意的方式控制设备,并打造精彩的自动化场景!

后续步骤

  • 请参阅问题排查,了解如何有效地调试应用并排查涉及 Home API 的问题。
  • 如果您有任何建议,或者想报告任何问题,都可以通过问题跟踪器(智能家居支持主题)与我们联系。