在 Android 上使用 Google Home API 建立自動化動作

1. 事前準備

這是使用 Google Home API 建構 Android 應用程式的系列程式碼研究室第二篇。在本程式碼研究室中,我們將逐步說明如何建立住家自動化動作,並提供使用 API 的最佳做法訣竅。如果您尚未完成第一個程式碼研究室「使用 Android 上的 Home API 建構行動應用程式」,建議先完成該研究室,再開始進行本程式碼研究室。

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 互動的服務。

在本程式碼研究室中,我們將延續「使用 Android 上的 Home API 建構行動應用程式」所涵蓋的內容。

請確認帳戶中至少有兩個支援的裝置,且已設定並正常運作。在本程式碼研究室中,我們將設定自動化功能 (裝置狀態變更會觸發另一個裝置的動作),因此您需要兩部裝置才能查看結果。

取得範例應用程式

您可以在 GitHub 的 google-home/google-home-api-sample-app-android 存放區中找到範例應用程式的原始碼。

本程式碼研究室會使用範例應用程式 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 建構行動應用程式」中使用的分支不同。這個程式碼基底分支版本是第一個程式碼研究室的延伸內容。這次的範例將逐步說明如何建立自動化作業。如果您已完成先前的程式碼研究室,且所有功能都能正常運作,可以選擇使用同一個 Android Studio 專案完成本程式碼研究室,而不使用 codelab-branch-2

編譯來源程式碼並準備在行動裝置上執行後,請繼續進行下一節。

3. 瞭解自動化功能

自動化動作是一組「如果發生這種情況,就執行這個動作」的陳述式,可根據所選因素自動控制裝置狀態。開發人員可使用自動化功能,在 API 中建構進階互動式功能。

自動化動作由三種不同類型的元件組成,稱為「節點」:啟動條件、動作和限制條件。這些節點會搭配運作,使用智慧住宅裝置自動執行動作。系統通常會依下列順序評估:

  1. 啟動條件:定義啟動自動化動作的初始條件,例如特徵值變更。自動化動作必須有啟動條件
  2. 條件:自動化動作觸發後,要評估的任何額外限制。條件中的運算式必須評估為 true,自動化動作才能執行。
  3. 動作:符合所有條件時執行的指令或狀態更新。

舉例來說,你可以設定自動化動作,在切換開關時調暗房間的燈光,同時開啟房間的電視。在這個例子中:

  • 啟動器:切換房間內的開關。
  • 條件:評估電視的開關狀態為「開啟」。
  • 動作:調暗與智慧開關位於同一房間的燈光。

自動化引擎會以串聯或並聯方式評估這些節點。

image5.png

「Sequential Flow」(循序流程) 包含依序執行的節點。通常是啟動條件、限制條件和動作。

image6.png

平行流程可能會同時執行多個動作節點,例如同時開啟多盞燈。並行流程完成前,後續節點不會執行。

自動化架構中還有其他類型的節點。如要進一步瞭解節點,請參閱 Home API 開發人員指南的「節點」一節。此外,開發人員可以結合不同類型的節點,建立複雜的自動化程序,例如:

image13.png

開發人員會使用專為 Google Home 自動化動作建立的領域特定語言 (DSL),將這些節點提供給 Automation Engine。

探索 Automation 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

請務必為自動化動作命名,然後輕觸「Create Automation」(建立自動化動作) 按鈕,系統應會呼叫 API,並將您帶回自動化動作清單檢視畫面,其中會顯示您的自動化動作:

8eebc32cd3755618.png

輕觸剛建立的自動化動作,查看 API 傳回的結果。

931dba7c325d6ef7.png

請注意,API 會傳回值,指出自動化功能是否有效且目前是否啟用。在伺服器端剖析自動化動作時,可能會建立未通過驗證的自動化動作。如果自動化作業剖析驗證失敗,isValid 會設為 false,表示自動化作業無效且處於非使用中狀態。如果自動化功能無效,請查看 automation.validationIssues 欄位瞭解詳情。

確認自動化動作已設為有效並啟用,然後即可試用。

試用自動化動作

自動化作業的執行方式有兩種:

  1. 使用啟動條件事件。如果符合條件,系統就會觸發您在自動化動作中設定的動作。
  2. 透過手動執行 API 呼叫。

如果自動化草稿在自動化草稿 DSL 區塊中定義了 manualStarter(),自動化引擎就會支援手動執行該自動化作業。範例應用程式的程式碼範例中已提供這項功能。

由於行動裝置仍停留在自動化檢視畫面,請輕觸「手動執行」按鈕。這應該會呼叫 automation.execute(),在設定自動化動作時選取的裝置上執行動作指令。

透過 API 手動執行驗證動作指令後,現在可以查看是否也使用您定義的啟動器執行。

前往「Devices」分頁,選取動作裝置和特徵,然後將其設為不同值 (例如將 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 探索可用的指令。

探索指令

在本節中,我們將討論如何探索支援的 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 行動裝置可以播放 Google 助理廣播)。例如:

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

Automation 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 應用程式。在本程式碼研究室中,您已瞭解 Automation 和 Discovery API。

希望您能盡情使用 Home API,建構可創意控制 Google Home 生態系統裝置的應用程式,並打造令人期待的自動化情境!

後續步驟

  • 請參閱「疑難排解」一文,瞭解如何有效偵錯應用程式,以及排解與 Home API 相關的問題。
  • 如有任何建議或要回報問題,請透過 Issue Tracker 的智慧住宅支援主題與我們聯絡。