기기 검색 활용

Discovery API는 사용자의 홈에 있는 기기를 기반으로 자동화를 만들 수 있는 앱에서 사용하도록 설계되었습니다. 자동화에 사용할 수 있도록 특정 구조에 있는 트레잇과 기기를 런타임 시 앱에 표시할 수 있습니다. 또한 관련 명령어, 속성, 이벤트는 물론 매개변수 및 필드에 허용되는 값 범위를 노출합니다.

Discovery API는 Automation API에서 지원되지 않는 구조에 있는 기기 또는 트레잇과 FactoryRegistry에 등록되지 않은 기기 또는 트레잇을 무시합니다. FactoryRegistry 사용에 관한 자세한 내용은 Home 인스턴스 만들기를 참고하세요.

API 사용

Discovery API의 핵심은 Structure, Room, HomeDevice를 포함하는 유형 계층 구조의 루트인 HasCandidates 인터페이스입니다.

HasCandidates 인터페이스는 모두 Flow 객체를 반환하는 candidates()allCandidates()라는 두 가지 메서드를 정의합니다.

  • candidates()는 항목(Structure, Room, HomeDevice)의 자동화 후보 목록을 생성합니다.

  • allCandidates()는 항목 및 모든 하위 항목의 자동화 후보 목록을 생성합니다. 이 메서드는 Room에서 지원되지 않습니다.

Home API에서 반환하는 다른 Flow 객체와 달리 일회성 스냅샷이 포함됩니다.

사용 가능한 후보의 최신 목록을 가져오려면 개발자가 매번 candidates() 또는 allCandidates()를 호출해야 하며 Flow 객체에서 collect()를 호출할 수 없습니다. 또한 이 두 메서드는 특히 리소스 집약적이므로 1분에 한 번 이상 호출하면 캐시된 데이터가 반환되며, 이 데이터는 그 순간의 실제 현재 상태를 반영하지 않을 수 있습니다.

NodeCandidate 인터페이스는 이러한 두 메서드에서 찾은 후보 노드를 나타내며 다음 인터페이스가 포함된 계층 구조의 루트입니다.

다음 클래스:

자동화 후보 작업

사용자가 지정한 시간에 스마트 블라인드를 닫는 자동화를 만드는 앱을 작성한다고 가정해 보겠습니다. 하지만 사용자에게 WindowCovering 트레잇을 지원하는 기기가 있는지, WindowCovering 또는 그 속성이나 명령어를 자동화에 사용할 수 있는지 여부는 알 수 없습니다.

다음 코드는 Discovery API를 사용하여 candidates() 메서드의 출력을 필터링하여 결과를 좁히고 찾고 있는 특정 종류의 요소 (구조, 이벤트, 명령)를 가져오는 방법을 보여줍니다. 마지막으로 수집된 요소로 자동화를 만듭니다.

import com.google.home.Structure
import com.google.home.automation.CommandCandidate
import com.google.home.automation.EventCandidate
import com.google.home.automation.Automation
import com.google.home.automation.DraftAutomation
import com.google.home.platform.Time
import java.time.LocalTime
import com.google.home.matter.standard.WindowCoveringTrait
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

fun createAutomationWithDiscoveryApiTimeStarter(
  structureName: String,
  scheduledTimeInSecond: Int,
): String = runBlocking {

  // get the Structure
  val structure = homeClient.structures().findStructureByName(structureName)

  // get an event candidate
  val clockTimeStarter =
    structure
      .allCandidates().first().firstOrNull {candidate ->
        candidate is EventCandidate && candidate.eventFactory == Time.ScheduledTimeEvent
      } as EventCandidate

  // retrieve the first 'DownOrClose' command encountered
  val downOrCloseCommand =
    structure.allCandidates().first().firstOrNull {
        candidate ->
      candidate is CommandCandidate
        && candidate.commandDescriptor == WindowCoveringTrait.DownOrCloseCommand
    } as CommandCandidate

  val blinds = ...

  // prompt user to select the WindowCoveringDevice
  ...

if (clockTimeStarter && downOrCloseCommand && blinds) {
  // Create the draft automation
  val draftAutomation: DraftAutomation = automation {
    name = ""
    description = ""
    isActive = true
    sequential {
      val mainStarter = starter<_>(structure, Time.ScheduledTimeEvent) {
          parameter(
            Time.ScheduledTimeEvent.clockTime(
              LocalTime.ofSecondOfDay(scheduledTimeInSecond.toLong())
            )
          )
        }
      action(blinds, WindowCoveringDevice) { command(WindowCoveringTrait.downOrClose())
    }
  }

  // Create the automation in the structure
  val automation = structure.createAutomation(draftAutomation)
  return@runBlocking automation.id.id
} else  ... //the automation cannot be created

다음 예에서는 조명이 켜질 때 조명의 밝기 수준을 설정하는 자동화를 만듭니다.

import com.google.home.Structure
import com.google.home.automation.CommandCandidate
import com.google.home.automation.TraitAttributesCandidate
import com.google.home.automation.Automation
import com.google.home.automation.DraftAutomation
import com.google.home.matter.standard.LevelControl
import com.google.home.matter.standard.LevelControlTrait.MoveToLevelCommand
import com.google.home.matter.standard.OnOff
import com.google.home.matter.standard.OnOff.Companion.onOff
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import com.google.home.automation.equals

fun createAutomationWithDiscoveryApiDimLight(
  structureName: String,
): String = runBlocking {

  // get the Structure
  val structure: Structure = homeClient.structures().findStructureByName(structureName)

  /**
   * When I turn on the light, move the brightness level to 55
   */
  val allCandidates = structure.allCandidates().first()
  val dimmableLightDevice = structure.devices().list().first {it.has(OnOff) && it.has(LevelControl)}
  val starterCandidate =
    allCandidates
      .filterIsInstance<TraitAttributesCandidate>()
      .first { it.entity == dimmableLightDevice && it.trait == OnOff }

  val actionCandidate =
    allCandidates
      .filterIsInstance<CommandCandidate>()
      .first {it.entity == dimmableLightDevice && it.commandDescriptor == MoveToLevelCommand }

  if (starterCandidate && actionCandidate) {
    // Create the draft automation
    val draftAutomation: DraftAutomation = automation {
      sequential {
        val starter =  starter<_>(dimmableLightDevice, OnOffLightDevice, OnOff)
        condition { expression = starter.onOff equals true }
        action(dimmableLightDevice,DimmableLightDevice) {
          mapOf(MoveToLevelCommand.Request.CommandFields.level to 55u.toUByte())
        )
      }
    }

    // Create the automation in the structure
    val automation = structure.createAutomation(draftAutomation)
    return@runBlocking automation.id.id
    }
} else ... //the automation cannot be created

기본 요건 확인

Discovery API를 사용하면 트레잇에 사용을 위한 기본 요건(예: 정기 결제 또는 구조 주소)이 누락되었음을 알 수 있습니다. Candidate 클래스의 unsupportedReasons 속성을 사용하여 이 작업을 수행합니다. 이 속성은 candidates() 호출 중에 UnsupportedCandidateReason로 채워집니다. createAutomation()가 호출될 때 유효성 검사 오류 메시지에도 동일한 정보가 표시됩니다.

이유 예시:

  • MissingStructureAddressSetupTime 트레잇을 사용하려면 주소 설정이 필요하다고 사용자에게 알립니다. Google 집 주소 변경에서는 사용자가 Google Home app (GHA)를 사용하여 구조물 주소를 입력하는 방법을 설명합니다.
  • MissingPresenceSensingSetup: AreaPresenceStateAreaAttendanceState 트레잇을 사용하려면 상태 설정이 필요하다고 사용자에게 알립니다.
  • MissingSubscriptionObjectDetection 트레잇을 사용하려면 Nest Aware 요금제에 가입해야 한다고 사용자에게 알립니다.

예를 들어 MissingStructureAddressSetup UnsupportedCandidateReason를 처리하려면 앱에 토스트를 표시하고 GHA를 열어 사용자가 구조체의 주소를 제공할 수 있도록 할 수 있습니다.

val structure = homeManager.structures().list().single()
val allCandidates = structure.allCandidates().list().single()
val scheduledStarterCandidate = allCandidates.first { it is EventCandidate && it.eventFactory == ScheduledTimeEvent }
if (scheduledStarterCandidate.unsupportedReasons.any { it is MissingStructureAddressSetup }) {
  showToast("No Structure Address setup. Redirecting to GHA to set up an address.")
  launchChangeAddress(...)
}

매개변수 유효성 검사

Discovery API는 속성, 매개변수 또는 이벤트 필드에 허용되는 값을 Constraint 인스턴스 형식으로 반환합니다. 이 정보를 통해 앱 개발자는 사용자가 잘못된 값을 설정하지 못하도록 할 수 있습니다.

Constraint의 각 서브클래스에는 허용되는 값을 나타내는 고유한 방법이 있습니다.

표: Constraint 유형
제약 조건 클래스 허용되는 값을 나타내는 속성
BitmapConstraint combinedBits
BooleanConstraint
ByteConstraint maxLengthminLength
EnumConstraint allowedSet
NumberRangeConstraint lowerBound, upperBound, step, unit
NumberSetConstraint allowedSetunit
StringConstraint allowedSet, disallowedSet, isCaseSensitive, maxLength, minLength, regex
StructConstraint fieldConstraints
ListConstraint elementConstraint

제약 조건 사용

LevelControl 트레잇으로 기기의 수준을 설정하는 자동화를 만드는 앱을 작성한다고 가정해 보겠습니다. 다음 예는 LevelControl 트레잇의 currentLevel 속성을 설정하는 데 사용된 값이 허용된 범위 내에 있는지 확인하는 방법을 보여줍니다.


import android.content.Context
import com.google.home.Home
import com.google.home.Structure
import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.CommandCandidate
import com.google.home.automation.Condition
import com.google.home.automation.Constraint
import com.google.home.automation.Equals
import com.google.home.automation.EventCandidate
import com.google.home.automation.HasCandidates
import com.google.home.automation.Node
import com.google.home.automation.NodeCandidate
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.matter.standard.LevelControlTrait

// Filter the output of candidates() to find the TraitAttributesCandidate
// for the LevelControl trait.

val levelCommand =
        structure
          .allCandidates()
          .first()
          .firstOrNull { candidate ->
            candidate is CommandCandidate && candidate.command == LevelControlTrait.MoveToLevelCommand
 } as? CommandCandidate

var levelConstraint = null

// Get the NodeCandidate instance's fieldDetailsMap and
// retrieve the Constraint associated with the level parameter.
// In this case, it is a NumberRangeConstraint.
if (levelCommand != null) {
    levelConstraint =
      levelCommand.fieldDetailsMap[
        LevelControlTrait.MoveToLevelCommand.Request.CommandFields.level
        ]!!.constraint
}

...

// Test the value against the Constraint (ignoring step and unit)
if ( value in levelConstraint.lowerBound..levelConstraint.upperBound) {
   // ok to use the value
}

Device API와 Discovery API 비교

Discovery API를 사용하지 않고도 기기 유형, 트레잇, 속성을 검색할 수 있습니다. Device API를 사용하면 다음을 찾을 수 있습니다.

  1. 사용자가 DeviceType.Metadata.isPrimaryType() 메서드를 사용하여 개발자에게 제어 권한을 부여한 기본 기기 유형입니다.
  2. 각 기기가 HasTraits.has() 메서드를 사용하여 자동화에 필요한 모든 트레잇을 지원하는지 여부
  3. 각 트레잇이 supports() 메서드를 사용하여 자동화에 필요한 모든 속성명령어를 지원하는지 여부

Device API를 사용하여 검색을 실행하는 경우 다음 Discovery API 기능을 사용할 수 없습니다.

  • 자동화 API에서 지원되지 않는 트레잇을 자동으로 필터링합니다.
  • 사용자에게 제약 조건을 사용하는 속성 및 매개변수의 유효한 값을 선택할 수 있는 옵션을 제공하는 기능입니다.