1. 事前準備
這是使用 Google Home API 建構 Android 應用程式的系列程式碼研究室第二篇。在本程式碼研究室中,我們將逐步說明如何建立住家自動化動作,並提供使用 API 的最佳做法訣竅。如果您尚未完成第一個程式碼研究室「使用 Android 上的 Home API 建構行動應用程式」,建議先完成該研究室,再開始進行本程式碼研究室。
Google Home API 提供一組程式庫,供 Android 開發人員在 Google Home 生態系統中控制智慧住宅裝置。有了這些新 API,開發人員就能為智慧住宅設定自動化動作,根據預先定義的條件控制裝置功能。Google 也提供 Discovery API,可讓您查詢裝置,瞭解裝置支援哪些屬性和指令。
必要條件
- 完成「使用 Android 上的 Home API 建構行動應用程式」程式碼研究室。
- 瞭解 Google Home 生態系統 (雲端對雲端和 Matter)。
- 已安裝 Android Studio (2024.3.1 Ladybug 以上版本) 的工作站。
- 符合 Home API 規定 (請參閱「必要條件」) 的 Android 手機,並已安裝 Google Play 服務和 Google Home 應用程式。模擬器無法運作,範例應用程式僅支援實體 Android 手機。
- 支援 Google Home API 的相容 Google Home Hub。
- 選用 - 與 Google Home API 相容的智慧住宅裝置。
課程內容
- 如何使用 Home API 建立智慧住宅裝置的自動化動作。
- 如何使用 Discovery API 探索支援的裝置功能。
- 瞭解如何使用 Home API 建構應用程式時,採用最佳做法。
2. 設定專案
下圖說明 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 中建構進階互動式功能。
自動化動作由三種不同類型的元件組成,稱為「節點」:啟動條件、動作和限制條件。這些節點會搭配運作,使用智慧住宅裝置自動執行動作。系統通常會依下列順序評估:
- 啟動條件:定義啟動自動化動作的初始條件,例如特徵值變更。自動化動作必須有啟動條件。
- 條件:自動化動作觸發後,要評估的任何額外限制。條件中的運算式必須評估為 true,自動化動作才能執行。
- 動作:符合所有條件時執行的指令或狀態更新。
舉例來說,你可以設定自動化動作,在切換開關時調暗房間的燈光,同時開啟房間的電視。在這個例子中:
- 啟動器:切換房間內的開關。
- 條件:評估電視的開關狀態為「開啟」。
- 動作:調暗與智慧開關位於同一房間的燈光。
自動化引擎會以串聯或並聯方式評估這些節點。
「Sequential Flow」(循序流程) 包含依序執行的節點。通常是啟動條件、限制條件和動作。
平行流程可能會同時執行多個動作節點,例如同時開啟多盞燈。並行流程完成前,後續節點不會執行。
自動化架構中還有其他類型的節點。如要進一步瞭解節點,請參閱 Home API 開發人員指南的「節點」一節。此外,開發人員可以結合不同類型的節點,建立複雜的自動化程序,例如:
開發人員會使用專為 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()) }
}
}
在上述範例中,自動化動作會同步處理兩個燈泡。當 device1
的 OnOff
狀態變更為 On
(onOffTrait.onOff equals true
) 時,device2
的 OnOff
狀態會變更為 On
(command(OnOff.on()
)。
使用自動化功能時,請注意資源限制。
自動化動作是建立智慧型住宅自動化功能的實用工具。在最基本的使用案例中,您可以明確編寫自動化程式碼,使用特定裝置和特徵。但更實用的用途是讓使用者設定自動化作業的裝置、指令和參數。下一節將說明如何建立自動化編輯器,讓使用者執行這項操作。
4. 建構自動化編輯器
在範例應用程式中,我們會建立自動化編輯器,讓使用者選取裝置、要使用的功能 (動作),以及如何使用啟動器觸發自動化程序。
設定啟動條件
自動化動作啟動條件是自動化動作的進入點。發生特定事件時,啟動條件會觸發自動化動作。在範例應用程式中,我們會使用 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)) }
執行應用程式並選取新的自動化動作和啟動器後,應該會顯示如下的檢視畫面:
範例應用程式僅支援以裝置特徵為基礎的入門套件。
設定動作
自動化動作反映自動化的主要目的,也就是在實體世界中產生變化。在範例應用程式中,我們使用 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)) }
執行應用程式並選取新的自動化和動作後,應該會看到如下畫面:
在範例應用程式中,我們僅支援根據裝置特徵執行的動作。
轉譯草稿自動化動作
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
}
}
現在執行應用程式,看看裝置上的變化!
選取啟動條件和動作後,即可建立自動化動作:
請務必為自動化動作命名,然後輕觸「Create Automation」(建立自動化動作) 按鈕,系統應會呼叫 API,並將您帶回自動化動作清單檢視畫面,其中會顯示您的自動化動作:
輕觸剛建立的自動化動作,查看 API 傳回的結果。
請注意,API 會傳回值,指出自動化功能是否有效且目前是否啟用。在伺服器端剖析自動化動作時,可能會建立未通過驗證的自動化動作。如果自動化作業剖析驗證失敗,isValid
會設為 false
,表示自動化作業無效且處於非使用中狀態。如果自動化功能無效,請查看 automation.validationIssues
欄位瞭解詳情。
確認自動化動作已設為有效並啟用,然後即可試用。
試用自動化動作
自動化作業的執行方式有兩種:
- 使用啟動條件事件。如果符合條件,系統就會觸發您在自動化動作中設定的動作。
- 透過手動執行 API 呼叫。
如果自動化草稿在自動化草稿 DSL 區塊中定義了 manualStarter()
,自動化引擎就會支援手動執行該自動化作業。範例應用程式的程式碼範例中已提供這項功能。
由於行動裝置仍停留在自動化檢視畫面,請輕觸「手動執行」按鈕。這應該會呼叫 automation.execute()
,在設定自動化動作時選取的裝置上執行動作指令。
透過 API 手動執行驗證動作指令後,現在可以查看是否也使用您定義的啟動器執行。
前往「Devices」分頁,選取動作裝置和特徵,然後將其設為不同值 (例如將 light2
的 LevelControl
(亮度) 設為 50%,如下列螢幕截圖所示:
現在我們將嘗試使用啟動裝置觸發自動化動作。選擇建立自動化動作時選取的啟動裝置。切換所選特徵 (例如將 starter outlet1
的 OnOff
設為 On
):
您會看到這也會執行自動化作業,並將動作裝置 light2
的 LevelControl
特徵設定為原始值 100%:
恭喜!您已成功使用 Home API 建立自動化動作!
如要進一步瞭解 Automation API,請參閱 Android Automation API。
5. 探索功能
Home API 包含專用的 Discovery API,開發人員可使用這項 API 查詢特定裝置支援哪些自動化功能。範例應用程式提供一個範例,說明如何使用這個 API 探索可用的指令。
探索指令
在本節中,我們將討論如何探索支援的 CommandCandidates
,以及如何根據探索到的候選節點建立自動化程序。
在範例應用程式中,我們會呼叫 device.candidates()
來取得候選項目清單,其中可能包含 CommandCandidate
、EventCandidate
或 TraitAttributesCandidate
的執行個體。
前往 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,並篩選出範例應用程式支援的結果,接下來將討論如何將這項功能整合到編輯器中。
如要進一步瞭解 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」,系統會提示您根據候選者的指令建立新的自動化動作:
您已熟悉如何在範例應用程式中建立自動化動作,現在可以在應用程式中整合自動化動作。
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!!")
)
}
}
}
使用 DelayFor
和 suppressFor
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 的智慧住宅支援主題與我們聯絡。