Android에서 Home API를 사용하여 자동화 만들기

1. 시작하기 전에

이 Codelab은 Google Home API를 사용하여 Android 앱을 빌드하는 시리즈의 두 번째 Codelab입니다. 이 Codelab에서는 홈 자동화를 만드는 방법을 살펴보고 API 사용에 관한 권장사항을 몇 가지 제공합니다. 아직 첫 번째 Codelab인 Android에서 Home API를 사용하여 모바일 앱 빌드를 완료하지 않았다면 이 Codelab을 시작하기 전에 해당 Codelab을 완료하는 것이 좋습니다.

Google Home API는 Android 개발자가 Google Home 생태계 내에서 스마트 홈 기기를 제어할 수 있는 라이브러리 세트를 제공합니다. 이러한 새로운 API를 통해 개발자는 사전 정의된 조건을 기반으로 기기 기능을 제어할 수 있는 스마트 홈의 자동화를 설정할 수 있습니다. Google에서는 기기를 쿼리하여 지원하는 속성과 명령어를 확인할 수 있는 Discovery API도 제공합니다.

기본 요건

학습할 내용

  • Home API를 사용하여 스마트 홈 기기를 자동화하는 방법
  • 디스커버리 API를 사용하여 지원되는 기기 기능을 탐색하는 방법
  • Home API로 앱을 빌드할 때 권장사항을 적용하는 방법

2. 프로젝트 설정

다음 다이어그램은 Home API 앱의 아키텍처를 보여줍니다.

Android 앱의 Home API 아키텍처

  • 앱 코드: 개발자가 앱의 사용자 인터페이스와 Home APIs SDK와 상호작용하는 로직을 빌드하기 위해 작업하는 핵심 코드입니다.
  • Home APIs SDK: Google에서 제공하는 Home APIs SDK는 GMSCore의 Home APIs 서비스와 함께 작동하여 스마트 홈 기기를 제어합니다. 개발자는 Home API SDK와 번들로 묶어 Home API와 호환되는 앱을 빌드합니다.
  • Android의 GMSCore: GMSCore(Google Play 서비스라고도 함)는 핵심 시스템 서비스를 제공하는 Google 플랫폼으로, 모든 인증된 Android 기기에서 주요 기능을 구현할 수 있습니다. Google Play 서비스의 홈 모듈에는 홈 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. 자동화 알아보기

자동화는 선택한 요인에 따라 기기 상태를 자동으로 제어할 수 있는 '이러한 경우 저러함' 문장의 집합입니다. 개발자는 자동화를 사용하여 API에 고급 상호작용 기능을 빌드할 수 있습니다.

자동화는 시작 조건, 작업, 조건이라는 세 가지 유형의 구성요소인 노드로 구성됩니다. 이러한 노드는 스마트 홈 기기를 사용하여 동작을 자동화하기 위해 함께 작동합니다. 일반적으로 다음과 같은 순서로 평가됩니다.

  1. 시작 조건 - 특성 값의 변경과 같이 자동화를 활성화하는 초기 조건을 정의합니다. 자동화에는 시작 조건이 있어야 합니다.
  2. 조건 - 자동화가 트리거된 후 평가할 추가 제약 조건입니다. 자동화의 작업을 실행하려면 조건의 표현식이 true로 평가되어야 합니다.
  3. 작업: 모든 조건이 충족될 때 실행되는 명령어 또는 상태 업데이트입니다.

예를 들어 스위치가 전환되고 방의 TV가 켜져 있을 때 방의 조명을 어둡게 하는 자동화를 설정할 수 있습니다. 이 예에서는 다음과 같이 정의됩니다.

  • 시작 - 방에 있는 스위치가 전환됩니다.
  • 조건: TV OnOff 상태가 On으로 평가됩니다.
  • 작업 — 스위치와 같은 방의 조명이 어두워집니다.

이러한 노드는 자동화 엔진에 의해 직렬 또는 병렬 방식으로 평가됩니다.

image5.png

순차적 흐름에는 순차적으로 실행되는 노드가 포함됩니다. 일반적으로 시작 조건, 조건, 작업이 이에 해당합니다.

image6.png

병렬 흐름에는 여러 조명을 동시에 켜는 등 동시에 실행되는 여러 작업 노드가 있을 수 있습니다. 병렬 흐름을 따르는 노드는 병렬 흐름의 모든 브랜치가 완료될 때까지 실행되지 않습니다.

자동화 스키마에는 다른 유형의 노드가 있습니다. 자세한 내용은 Home API 개발자 가이드의 노드 섹션을 참고하세요. 또한 개발자는 다양한 유형의 노드를 결합하여 다음과 같은 복잡한 자동화를 만들 수 있습니다.

image13.png

개발자는 Google Home 자동화를 위해 특별히 생성된 도메인별 언어 (DSL)를 사용하여 이러한 노드를 자동화 엔진에 제공합니다.

자동화 DSL 살펴보기

도메인 특정 언어 (DSL)는 코드에서 시스템 동작을 캡처하는 데 사용되는 언어입니다. 컴파일러는 프로토콜 버퍼 JSON으로 직렬화되고 Google의 자동화 서비스에 대한 호출을 만드는 데 사용되는 데이터 클래스를 생성합니다.

DSL은 다음 스키마를 찾습니다.

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

위의 예시에서 자동화는 두 개의 전구를 동기화합니다. device1OnOff 상태가 On (onOffTrait.onOff equals true)로 변경되면 device2OnOff 상태가 On (command(OnOff.on())로 변경됩니다.

자동화 작업을 할 때는 리소스 한도가 있다는 점에 유의하세요.

자동화는 스마트 홈에서 자동화된 기능을 만드는 데 매우 유용한 도구입니다. 가장 기본적인 사용 사례에서는 특정 기기와 특성을 사용하도록 자동화를 명시적으로 코딩할 수 있습니다. 하지만 더 실용적인 사용 사례는 앱에서 사용자가 자동화의 기기, 명령어, 매개변수를 구성할 수 있는 경우입니다. 다음 섹션에서는 사용자가 정확히 이 작업을 할 수 있는 자동화 편집기를 만드는 방법을 설명합니다.

4. 자동화 편집기 빌드

샘플 앱 내에서 사용자가 기기, 사용하려는 기능 (작업), 스타터를 사용하여 자동화가 트리거되는 방식을 선택할 수 있는 자동화 편집기를 만듭니다.

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

시작 조건 설정

자동화 시작 조건은 자동화의 진입점입니다. 스타터는 지정된 이벤트가 발생할 때 자동화를 트리거합니다. 샘플 앱에서는 StarterViewModel.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)) }

앱을 실행하고 새 자동화 및 스타터를 선택하면 다음과 같은 뷰가 표시됩니다.

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)
}

자동화 만들기

이제 시작 프로그램과 작업을 만드는 방법을 배웠으므로 자동화 초안을 만들어 자동화 API로 보낼 수 있습니다. API에는 자동화 초안을 인수로 사용하고 새 자동화 인스턴스를 반환하는 createAutomation() 함수가 있습니다.

초안 자동화 준비는 샘플 앱의 DraftViewModel 클래스에서 이루어집니다. getDraftAutomation() 함수를 살펴보면 이전 섹션의 스타터 및 작업 변수를 사용하여 자동화 초안을 구성하는 방법을 자세히 알 수 있습니다.

DraftViewModel.kt 파일에서 4.4.1단계를 주석 해제하여 시작 특성이 OnOff일 때 자동화 그래프를 만드는 데 필요한 'select' 표현식을 만듭니다.

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
  }
}

이제 앱을 실행하고 기기에서 변경사항을 확인하세요.

시작 조건과 작업을 선택하면 자동화를 만들 수 있습니다.

ec551405f8b07b8e.png

자동화에 고유한 이름을 지정한 다음 자동화 만들기 버튼을 탭합니다. 그러면 API가 호출되고 자동화 목록 뷰로 돌아가 자동화가 표시됩니다.

8eebc32cd3755618.png

방금 만든 자동화를 탭하고 API에서 반환되는 방식을 확인합니다.

931dba7c325d6ef7.png

API는 자동화가 유효하고 현재 활성 상태인지 여부를 나타내는 값을 반환합니다. 서버 측에서 파싱될 때 유효성 검사를 통과하지 않는 자동화를 만들 수 있습니다. 자동화 파싱이 유효성 검사에 실패하면 isValidfalse로 설정되어 자동화가 유효하지 않고 비활성 상태임을 나타냅니다. 자동화가 유효하지 않으면 automation.validationIssues 필드에서 세부정보를 확인하세요.

자동화가 유효하고 활성 상태로 설정되어 있는지 확인한 후 자동화를 사용해 볼 수 있습니다.

자동화 사용해 보기

자동화는 다음 두 가지 방법으로 실행할 수 있습니다.

  1. 시작 이벤트 조건이 일치하면 자동화에서 설정한 작업이 트리거됩니다.
  2. 수동 실행 API 호출

자동화 초안에 자동화 초안 DSL 블록에 정의된 manualStarter()이 있으면 자동화 엔진은 해당 자동화의 수동 실행을 지원합니다. 샘플 앱의 코드 예시에는 이미 포함되어 있습니다.

휴대기기에서 자동화 보기 화면이 계속 표시되므로 수동 실행 버튼을 탭합니다. 이렇게 하면 자동화를 설정할 때 선택한 기기에서 작업 명령어를 실행하는 automation.execute()가 호출됩니다.

API를 사용하여 수동 실행을 통해 작업 명령어를 검증했다면 이제 정의한 스타터를 사용하여 실행되는지 확인할 차례입니다.

기기 탭으로 이동하여 작업 기기와 특성을 선택하고 다른 값으로 설정합니다. 예를 들어 다음 스크린샷에 표시된 대로 light2LevelControl(밝기)를 50%로 설정합니다.

d0357ec71325d1a8.png

이제 시작 기기를 사용하여 자동화를 트리거해 보겠습니다. 자동화를 만들 때 선택한 시작 기기를 선택합니다. 선택한 특성을 전환합니다 (예: starter outlet1OnOffOn로 설정).

230c78cd71c95564.png

이렇게 하면 자동화가 실행되고 작업 기기 light2LevelControl 특성이 원래 값인 100%로 설정됩니다.

1f00292128bde1c2.png

축하합니다. Home API를 사용하여 자동화를 만들었습니다.

자동화 API에 대해 자세히 알아보려면 Android 자동화 API를 참고하세요.

5. 기능 검색

홈 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를 호출하고 샘플 앱에서 지원하는 결과를 필터링할 수 있으므로 이를 편집기에 통합하는 방법을 알아보겠습니다.

8a2f0e8940f7056a.png

Discovery API에 대해 자세히 알아보려면 Android에서 기기 검색 활용을 참고하세요.

편집기 통합

발견된 작업을 사용하는 가장 일반적인 방법은 최종 사용자가 선택할 수 있도록 작업을 표시하는 것입니다. 사용자가 초안 자동화 필드를 선택하기 직전에 검색된 작업 목록을 표시할 수 있으며, 선택한 값에 따라 자동화 초안의 작업 노드를 미리 채울 수 있습니다.

CandidatesView.kt 파일에는 검색된 후보를 표시하는 뷰 클래스가 포함되어 있습니다. 5.2.1단계를 주석 해제하여 homeAppVM.selectedDraftVMcandidateVM로 설정하는 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을 탭하여 다시 시도합니다. 그러면 후보자의 명령에 따라 새 자동화를 만들라는 메시지가 표시됩니다.

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
  ...
  }
}

어시스턴트 브로드캐스트를 작업으로 사용

AssistantBroadcast 특성은 SpeakerDevice의 기기 수준 특성 (스피커가 지원하는 경우) 또는 구조 수준 특성으로 사용할 수 있습니다 (Google 스피커와 Android 휴대기기에서 어시스턴트 브로드캐스트를 재생할 수 있기 때문). 예를 들면 다음과 같습니다.

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

DelayForsuppressFor 사용

또한 자동화 API는 명령어를 지연하는 delayFor 및 지정된 시간 내에 동일한 이벤트에 의해 자동화가 트리거되지 않도록 억제할 수 있는 suppressFor와 같은 고급 연산자를 제공합니다. 다음은 이러한 연산자를 사용하는 몇 가지 예입니다.

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

스타터에서 AreaPresenceState 사용

AreaPresenceState는 집에 사람이 있는지 감지하는 구조 수준 특성입니다.

예를 들어 다음 예에서는 오후 10시 이후에 누군가 집에 있을 때 문을 자동으로 잠그는 방법을 보여줍니다.

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

이제 이러한 고급 자동화 기능을 잘 알게 되었으니 멋진 앱을 만들어 보세요.

7. 축하합니다.

축하합니다. Google Home API를 사용하여 Android 앱을 개발하는 두 번째 부분을 완료했습니다. 이 Codelab을 통해 자동화 및 검색 API를 살펴봤습니다.

Google Home 생태계 내에서 기기를 창의적으로 제어하고 Home API를 사용하여 흥미로운 자동화 시나리오를 빌드하는 앱을 개발해 보세요.

다음 단계

  • 문제 해결을 읽고 앱을 효과적으로 디버그하고 Home API와 관련된 문제를 해결하는 방법을 알아보세요.
  • Issue Tracker, 스마트 홈 지원 주제를 통해 추천사항을 문의하거나 문제를 신고할 수 있습니다.