1. 시작하기 전에
이 Codelab은 Google Home API를 사용하여 Android 앱을 빌드하는 방법을 다루는 시리즈의 두 번째 Codelab입니다. 이 Codelab에서는 홈 자동화를 만드는 방법을 설명하고 API를 사용하는 권장사항에 관한 몇 가지 도움말을 제공합니다. 첫 번째 Codelab인 Android에서 Home API를 사용하여 모바일 앱 빌드를 아직 완료하지 않았다면 이 Codelab을 시작하기 전에 완료하는 것이 좋습니다.
Google Home API는 Android 개발자가 Google Home 생태계 내에서 스마트 홈 기기를 제어할 수 있는 라이브러리 모음을 제공합니다. 개발자는 이러한 새로운 API를 사용하여 사전 정의된 조건에 따라 기기 기능을 제어할 수 있는 스마트 홈 자동화를 설정할 수 있습니다. 또한 Google에서는 기기를 쿼리하여 기기에서 지원하는 속성과 명령어를 확인할 수 있는 Discovery API를 제공합니다.
기본 요건
- Android에서 Home API를 사용하여 모바일 앱 빌드 Codelab을 완료합니다.
- Google Home 생태계 (클라우드 간 및 Matter)에 대한 지식
- Android 스튜디오 (2024.3.1 Ladybug 이상)가 설치된 워크스테이션
- Home API 요구사항 (기본 요건 참고)을 충족하고 Google Play 서비스 및 Google Home 앱이 설치된 Android 휴대전화
- Google Home API를 지원하는 호환되는 Google Home Hub
- 선택사항 - Google Home API와 호환되는 스마트 홈 기기
학습할 내용
- Home API를 사용하여 스마트 홈 기기의 자동화를 만드는 방법
- Discovery API를 사용하여 지원되는 기기 기능을 살펴보는 방법
- Home API로 앱을 빌드할 때 권장사항을 적용하는 방법
2. 프로젝트 설정
다음 다이어그램은 Home APIs 앱의 아키텍처를 보여줍니다.
- 앱 코드: 개발자가 앱의 사용자 인터페이스와 Home APIs SDK와 상호작용하는 로직을 빌드하는 데 사용하는 핵심 코드입니다.
- Home APIs SDK: Google에서 제공하는 Home APIs SDK는 GMSCore의 Home APIs 서비스와 함께 작동하여 스마트 홈 기기를 제어합니다. 개발자는 Home APIs를 Home APIs SDK와 번들로 묶어 Home APIs와 호환되는 앱을 빌드합니다.
- Android의 GMSCore: Google Play 서비스라고도 하는 GMSCore는 모든 인증된 Android 기기에서 핵심 기능을 사용 설정하는 핵심 시스템 서비스를 제공하는 Google 플랫폼입니다. Google Play 서비스의 Home 모듈에는 Home API와 상호작용하는 서비스가 포함되어 있습니다.
이 Codelab에서는 Android에서 Home API를 사용하여 모바일 앱 빌드에서 다룬 내용을 바탕으로 진행합니다.
계정에 설정되어 있고 작동하는 지원되는 기기가 2대 이상 있는 구조가 있어야 합니다. 이 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을 완료하고 모든 기능을 사용할 수 있었다면 codelab-branch-2
를 사용하는 대신 동일한 Android 스튜디오 프로젝트를 사용하여 이 Codelab을 완료할 수 있습니다.
소스 코드가 컴파일되고 휴대기기에서 실행할 준비가 되면 다음 섹션으로 진행합니다.
3. 자동화에 대해 알아보기
자동화는 선택한 요인에 따라 기기 상태를 자동화된 방식으로 제어할 수 있는 'if this, then that' 문의 집합입니다. 개발자는 자동화를 사용하여 API에 고급 양방향 기능을 빌드할 수 있습니다.
자동화는 nodes라고 하는 세 가지 유형의 구성요소(시작 조건, 작업, 조건)로 구성됩니다. 이러한 노드는 함께 작동하여 스마트 홈 기기를 사용하여 동작을 자동화합니다. 일반적으로 다음 순서로 평가됩니다.
- Starter: 트레잇 값 변경과 같이 자동화를 활성화하는 초기 조건을 정의합니다. 자동화에는 Starter이 있어야 합니다.
- 조건: 자동화가 트리거된 후 평가할 추가 제약 조건입니다. 자동화 작업이 실행되려면 조건의 표현식이 true로 평가되어야 합니다.
- 작업: 모든 조건이 충족될 때 실행되는 명령어 또는 상태 업데이트입니다.
예를 들어 스위치가 전환되었을 때 방의 조명을 어둡게 하고 방의 TV를 켜는 자동화를 설정할 수 있습니다. 이 예에서는 다음과 같이 정의됩니다.
- Starter: 방의 스위치가 전환됩니다.
- 조건: TV OnOff 상태가 켜짐으로 평가됩니다.
- 작업: 스위치와 같은 방의 조명이 어두워집니다.
이러한 노드는 자동화 엔진에서 직렬 또는 병렬 방식으로 평가됩니다.
순차 흐름에는 순차적으로 실행되는 노드가 포함됩니다. 일반적으로 시작 조건, 조건, 작업이 이에 해당합니다.
병렬 흐름에는 여러 조명 켜기와 같이 동시에 실행되는 여러 작업 노드가 있을 수 있습니다. 병렬 흐름을 따르는 노드는 병렬 흐름의 모든 브랜치가 완료될 때까지 실행되지 않습니다.
자동화 스키마에는 다른 유형의 노드도 있습니다. Home APIs 개발자 가이드의 노드 섹션에서 자세히 알아보세요. 또한 개발자는 다양한 유형의 노드를 결합하여 다음과 같은 복잡한 자동화를 만들 수 있습니다.
개발자는 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()) }
}
}
위의 예시에서 자동화는 두 개의 전구를 동기화합니다. 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()
함수를 살펴보고 이전 섹션의 시작 조건 및 작업 변수를 사용하여 자동화 초안을 구성하는 방법을 자세히 알아보세요.
시작 트레잇이 OnOff
인 경우 자동화 그래프를 만드는 데 필요한 'select' 표현식을 만들기 위해 DraftViewModel.kt
파일에서 4.4.1단계의 주석을 삭제합니다.
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()
...
}
선택한 작업 트레잇이 LevelControl
이고 선택한 작업이 MOVE_TO_LEVEL
인 경우 자동화 그래프를 만드는 데 필요한 병렬 표현식을 만들려면 DraftViewModel.kt
파일에서 4.4.2단계의 주석을 삭제합니다.
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%로 설정됩니다.
축하합니다. Home API를 사용하여 자동화를 만들었습니다.
Automation API에 관한 자세한 내용은 Android Automation API를 참고하세요.
5. 기능 살펴보기
Home API에는 개발자가 특정 기기에서 지원되는 자동화 가능한 트레잇을 쿼리하는 데 사용할 수 있는 Discovery API라는 전용 API가 포함되어 있습니다. 샘플 앱은 이 API를 사용하여 사용 가능한 명령어를 찾을 수 있는 예시를 제공합니다.
명령어 살펴보기
이 섹션에서는 지원되는 CommandCandidates
를 검색하는 방법과 검색된 후보 노드를 기반으로 자동화를 만드는 방법을 설명합니다.
샘플 앱에서는 device.candidates()
를 호출하여 후보 목록을 가져옵니다. 이 목록에는 CommandCandidate
, EventCandidate
또는 TraitAttributesCandidate
의 인스턴스가 포함될 수 있습니다.
HomeAppViewModel.kt
파일로 이동하여 후보 목록을 가져오고 Candidate
유형으로 필터링하는 5.1.1단계의 주석 처리를 삭제합니다.
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단계의 주석을 삭제하여 homeAppVM.selectedDraftVM
를 candidateVM
로 설정하는 CandidateListItem
의 .clickable{}
함수를 사용 설정합니다.
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
...
}
}
어시스턴트 브로드캐스트 as Action
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!!")
)
}
}
}
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 앱을 개발하는 두 번째 부분을 완료했습니다. 이 Codelab에서는 Automation API와 Discovery API를 살펴봤습니다.
Google Home 생태계 내에서 기기를 창의적으로 제어하고 Home API를 사용하여 흥미로운 자동화 시나리오를 빌드하는 앱을 만드는 데 도움이 되기를 바랍니다.
다음 단계
- 문제 해결을 읽고 앱을 효과적으로 디버그하고 Home API와 관련된 문제를 해결하는 방법을 알아보세요.
- 추천사항이 있으면 Google에 문의하거나 Issue Tracker, 스마트 홈 지원 주제를 통해 문제를 신고할 수 있습니다.