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 應用程式。
- 支援 Google Home API 的 Google Home Hub。
- 選用:與 Google Home API 相容的智慧住宅裝置。
課程內容
- 如何使用 Home API 為智慧住宅裝置建立自動化動作。
- 如何使用 Discovery API 探索支援的裝置功能。
- 如何在使用 Google Home API 建構應用程式時,採用最佳做法。
2. 設定專案
下圖說明 Home API 應用程式的架構:
- 應用程式程式碼:開發人員用來建構應用程式使用者介面和與 Home API SDK 互動邏輯的核心程式碼。
- Home APIs SDK:Google 提供的 Home APIs SDK 可搭配 GMSCore 中的 Home APIs Service 使用,用於控制智慧型家居裝置。開發人員可以將 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 上的 Google Home API 建構行動應用程式」中使用的分支不同。這個程式碼基底的分支版本是建立在第一個程式碼研究室結束的地方。這次的範例會逐步說明如何建立自動化動作。如果您已完成先前的程式碼研究室,並且能夠讓所有功能正常運作,您可以選擇使用相同的 Android Studio 專案完成本程式碼研究室,而非使用 codelab-branch-2
。
編譯完原始碼並準備在行動裝置上執行後,請繼續閱讀下一節。
3. 瞭解自動化動作
自動化動作是一組「如果這樣,就那樣」的陳述式,可根據所選因素以自動化方式控制裝置狀態。開發人員可以使用自動化功能,在 API 中建構進階互動功能。
自動化動作由三種不同類型的元件組成,稱為nodes:啟動條件、動作和限制條件。這些節點會搭配運作,透過智慧住宅裝置執行自動化動作。通常會依下列順序評估:
- Starter:定義啟動自動化動作的初始條件,例如特徵值變更。自動化動作必須有Starter。
- 限制條件:在自動化動作觸發後,要評估的任何其他限制條件。Condition 中的運算式必須評估為 true,自動化動作才能執行。
- 動作:在符合所有條件時執行的指令或狀態更新。
舉例來說,你可以建立自動化動作,在開啟該房間的電視時,同時調低房間燈光的亮度。在這個例子中:
- Starter:房間中的 Switch 已切換。
- 條件:系統會判定電視的 OnOff 狀態為 On。
- 動作:與開關所在房間相同的燈具會調暗。
自動化引擎會以序列或並行方式評估這些節點。
順序流程包含依序執行的節點。通常是啟動條件、限制條件和動作。
平行流程可能會同時執行多個動作節點,例如同時開啟多盞燈。並行流程的所有分支都完成後,才會執行並行流程後續的節點。
自動化動作結構定義中還有其他類型的節點。如要進一步瞭解這些元素,請參閱 Google Home API 開發人員指南的「節點」一節。此外,開發人員可以結合不同類型的節點,建立複雜的自動化動作,例如:
開發人員會使用專為 Google Home 自動化動作所建立的特定領域語言 (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.kt
來源檔案中的 StarterViewModel
類別擷取自動化啟動器,並使用 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
(draftVM.actionVMs
) 中儲存所有動作 ViewModel
的變數:
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,建立「select」運算式,以便在啟動條件特徵為 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
}
}
現在執行應用程式,看看裝置上的變更內容!
選取啟動條件和動作後,即可建立自動化動作:
請務必為自動化動作命名為不重複的名稱,然後輕觸「建立自動化動作」按鈕,系統就會呼叫 API,並帶您返回自動化動作清單檢視畫面,其中包含您的自動化動作:
輕觸剛建立的自動化動作,查看 API 傳回的結果。
請注意,API 會傳回值,指出自動化動作是否有效且目前處於啟用狀態。建立的自動化動作在伺服器端剖析時,可能無法通過驗證。如果自動化動作剖析作業驗證失敗,isValid
會設為 false
,表示自動化動作無效且處於停用狀態。如果自動化動作無效,請查看 automation.validationIssues
欄位瞭解詳情。
確認自動化動作已設為有效且啟用,然後試用自動化動作。
試用自動化動作
自動化動作的執行方式有兩種:
- 使用啟動條件事件。如果條件符合,系統就會觸發自動化動作中設定的動作。
- 使用手動執行 API 呼叫。
如果自動化草稿在自動化草稿 DSL 區塊中定義了 manualStarter()
,自動化引擎就會支援手動執行該自動化功能。範例應用程式的程式碼範例中已包含這項資訊。
由於你仍在行動裝置上的自動化動作檢視畫面,請輕觸「手動執行」按鈕。這應該會呼叫 automation.execute()
,在設定自動化動作時所選的裝置上執行動作指令。
使用 API 手動執行動作指令並驗證無誤後,現在可以確認是否也能使用您定義的啟動條件執行。
前往「裝置」分頁,選取動作裝置和特徵,然後將其設為不同的值 (例如將 light2
的 LevelControl
(亮度) 設為 50%,如以下螢幕截圖所示):
我們現在會嘗試使用啟動條件裝置觸發自動化動作。選擇建立自動化動作時選取的啟動裝置。切換所選特徵 (例如將 starter outlet1
的 OnOff
設為 On
):
您會發現這也會執行自動化動作,並將動作裝置 light2
的 LevelControl
特徵設為原始值 100%:
恭喜!您已成功使用 Google Home API 建立自動化動作!
如要進一步瞭解 Automation API,請參閱「Android Automation API」。
5. 探索功能
Google Home API 包含一個專用 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
自動化 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 API 和 Discovery API。
希望您能盡情享受建構應用程式的樂趣,並透過 Google Home API 創意控制 Google Home 生態系統中的裝置,以及建立令人驚豔的自動化情境!
後續步驟
- 請參閱疑難排解,瞭解如何有效偵錯應用程式,以及排解與 Google Home API 相關的問題。
- 如有任何建議,歡迎與我們聯絡,或透過 Issue Tracker 回報任何問題,這也是智慧型家居支援主題。