자동화 빌드

Automation API는 Home API를 통해 액세스할 수 있지만 진입점은 구조를 통해 이루어지므로 먼저 구조에 권한을 부여해야 사용할 수 있습니다.

구조에 권한이 부여되면 다음 패키지를 앱으로 가져옵니다.


import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.Id
import com.google.home.Structure

구조체에는 다음과 같은 자동화 관련 메서드가 있는 HasAutomations 인터페이스가 포함됩니다.

API 설명
automations() 구조에 속한 모든 자동화를 나열합니다. Home API를 통해 만든 자동화만 반환됩니다.
createAutomation(automation) 구조의 자동화 인스턴스를 만듭니다.
deleteAutomation(automationId) ID로 자동화 인스턴스를 삭제합니다.

자동화 만들기

Home 인스턴스를 만들고 사용자로부터 권한을 받은 후 구조 및 기기를 가져옵니다.

val structure = homeManager.structures().list().single()
val device = homeManager.devices().get(Id("myDevice"))!!

그런 다음 자동화 DSL을 사용하여 자동화 로직을 정의합니다. Home API에서 자동화는 Automation 인터페이스로 표현됩니다. 이 인터페이스에는 다음과 같은 속성 집합이 포함됩니다.

  • 이름, 설명과 같은 메타데이터
  • 예를 들어 자동화를 실행할 수 있는지 여부를 나타내는 플래그입니다.
  • 자동화 그래프라고 하는 자동화 로직이 포함된 노드 목록으로, automationGraph 속성으로 표시됩니다.

automationGraph는 기본적으로 순차적으로 실행되는 노드 목록이 포함된 클래스인 SequentialFlow 유형입니다. 각 노드는 시작 조건, 조건 또는 작업과 같은 자동화의 요소를 나타냅니다.

자동화에 namedescription를 할당합니다.

자동화를 만들면 기본적으로 isActive 플래그가 true로 설정되므로 처음에 자동화를 사용 중지하려는 경우가 아니라면 이 플래그를 명시적으로 설정할 필요가 없습니다. 이 시나리오에서는 생성 중에 플래그를 false로 설정합니다.

DraftAutomation 인터페이스는 자동화를 빌드하고 만드는 데 사용되고 Automation 인터페이스는 검색에 사용됩니다. 예를 들어 다른 기기가 켜지면 기기를 켜는 자동화에 관한 자동화 DSL은 다음과 같습니다.

import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.Condition
import com.google.home.automation.DraftAutomation
import com.google.home.automation.Equals
import com.google.home.automation.Node
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.matter.standard.OnOff
import com.google.home.Structure

...

val automation: DraftAutomation = automation {
  name = "MyFirstAutomation"
  description = "Turn on a device when another device is turned on."
  sequential {
    val starterNode = starter<_>(device1, OnOffLightDevice, trait=OnOff)
    condition() { expression = stateReaderNode.onOff equals true }
    action(device2, OnOffLightDevice) { command(OnOff.on()) }
  }
}

자동화 DSL이 정의되면 이를 createAutomation() 메서드에 전달하여 DraftAutomation 인스턴스를 만듭니다.

val createdAutomation = structure.createAutomation(automation)

여기에서 execute(), stop(), update()와 같은 자동화의 다른 모든 자동화 메서드를 사용할 수 있습니다.

유효성 검사 오류

자동화 생성이 유효성 검사를 통과하지 못하면 경고 또는 오류 메시지에 문제에 관한 정보가 제공됩니다. 자세한 내용은 ValidationIssueType 참조를 참고하세요.

코드 예시

다음은 자동화 설계 페이지에 설명된 가상 자동화의 일부를 구현하는 데 사용할 수 있는 코드 예입니다.

간단한 자동화

오전 8시에 블라인드를 올리는 자동화는 다음과 같이 구현할 수 있습니다.

// get all the automation node candidates in the structure
val allCandidates = structure.allCandidates().first()
// determine whether a scheduled automation can be constructed
val isSchedulingSupported =
  allCandidates.any {
    it is EventCandidate &&
      it.eventFactory == Time.ScheduledTimeEvent &&
      it.unsupportedReasons.isEmpty()
  }
// get the blinds present in the structure
val blinds =
  allCandidates
    .filter {
      it is CommandCandidate &&
        it.commandDescriptor == WindowCoveringTrait.UpOrOpenCommand &&
        it.unsupportedReasons.isEmpty()
    }
    .map { it.entity }
    .filterIsInstance<HomeDevice>()
    .filter { it.has(WindowCoveringDevice) }
 if (isSchedulingSupported && blinds.isNotEmpty()) {
  // Proceed to create automation
  val automation: DraftAutomation = automation {
    name = "Day time open blinds"
    description = "Open all blinds at 8AM everyday"
    isActive = true
    sequential {
      // At 8:00am local time....
      val unused =
        starter(structure, Time.ScheduledTimeEvent) {
          parameter(Time.ScheduledTimeEvent.clockTime(LocalTime.of(8, 0, 0, 0)))
        }
        // ...open all the blinds
       parallel {
        for (blind in blinds) {
          action(blind, WindowCoveringDevice) { command(WindowCovering.upOrOpen()) }
        }
      }
    }
  }
   val createdAutomation = structure.createAutomation(automation)
} else if (!isSchedulingSupported) {
  // Cannot create automation. Set up your address on the structure, then try again.
} else {
  // You don't have any WindowCoveringDevices. Try again after adding some blinds to your structure.
}

복잡한 자동화

움직임이 감지되면 조명이 깜박이도록 트리거하는 자동화는 다음과 같이 구현할 수 있습니다.

import com.google.home.Home
import com.google.home.HomeClient
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure
import com.google.home.automation.action
import com.google.home.automation.automation
import com.google.home.automation.equals
import com.google.home.automation.parallel
import com.google.home.automation.starter
import com.google.home.google.AssistantBroadcast
import com.google.home.matter.standard.OnOff
import com.google.home.matter.standard.OnOff.Companion.toggle
import com.google.home.matter.standard.OnOffLightDevice
import java.time.Duration

// get all the automation node candidates in the structure
val allCandidates = structure.allCandidates().first()

// get the lights present in the structure
val availableLights = allCandidates.filter {
   it is CommandCandidate &&
   it.commandDescriptor == OnOffTrait.OnCommand
}.map { it.entity }
.filterIsInstance<HomeDevice>()
.filter {it.has(OnOffLightDevice) ||
         it.has(ColorTemperatureLightDevice) ||
         it.has(DimmableLightDevice) ||
         it.has(ExtendedColorLightDevice)}

val selectedLights = ... // user selects one or more lights from availableLights

automation {
isActive = true

sequential {
   // If the presence state changes...
   val starterNode = starter<_>(structure, AreaPresenceState)
   // ...and if the area is occupied...
   condition() {
      expression = starterNode.presenceState equals PresenceState.PresenceStateOccupied
   }
   // "blink" the light(s)
   parallel {
            for(light in selectedLights) {
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle())}
         }
      }
   }
}

일반 자동화

다음 코드는 Structure API, Device API, Discovery API를 사용하여 사용자의 홈에 관한 데이터를 수집하고 사용자를 안내하여 자체 일반 자동화를 만드는 앱을 구현하는 방법을 보여줍니다 (자동화 설계: 일반 자동화 참고).

val structureFlow: Flow<Structure> = home.structures().itemFlow(myStructureId)

// Get a snapshot of the structure.
val structure: Structure = structureFlow.first()

   // map of devices where key is device id
val devicesById = mutableMapOf<Id,HomeDevice>()

home.devices().list().map { devicesById.put(it.id, it) }

// map of rooms where key is room id
val roomsByName: MutableMap<String,Room?> = mutableMapOf<String,Room?>()

structure.rooms().list().map { roomsByName.put(it.name, it) }

// map of commands where key is trait id
val commandsByTrait: MutableMap<String,MutableList<CommandCandidate>?> = mutableMapOf<String,MutableList<CommandCandidate>?>()

// map of commands where key is DeviceType.Metadata.hashcode()
val commandsByDeviceType: MutableMap<Id,MutableList<CommandCandidate>?> = mutableMapOf<Id,MutableList<CommandCandidate>?>()

// map of commands where key is entity id
val commandsByEntity: MutableMap<Id,MutableList<CommandCandidate>?> = mutableMapOf<Id,MutableList<CommandCandidate>?>()


// map of stateReaders where key is trait id
val stateReadersByTrait: MutableMap<Int,MutableList<StateReaderCandidate>?> = mutableMapOf<Int,MutableList<StateReaderCandidate>?>()

// map of stateReaders where key is DeviceType.Metadata.hashcode()
val stateReadersByDeviceType: MutableMap<Int,MutableList<StateReaderCandidate>?> = mutableMapOf<Int,MutableList<StateReaderCandidate>?>()

// map of stateReaders where key is  entity id
val stateReadersByEntity: MutableMap<Int,MutableList<StateReaderCandidate>?> = mutableMapOf<Int,MutableList<StateReaderCandidate>?>()

// map of starters where key is trait id
val startersByTrait: MutableMap<Int,MutableList<StarterCandidate>?> = mutableMapOf<Int,MutableList<StarterCandidate>?>()

// map of starters where key is DeviceType.Metadata.hashcode()
val startersByDeviceType: MutableMap<Int,MutableList<CommandCandidate>?> = mutableMapOf<Int,MutableList<CommandCandidate>?>()

// map of starters where key is entity id
val startersByEntity: MutableMap<Int,MutableList<StarterCandidate>?> = mutableMapOf<Int,MutableList<StarterCandidate>?>()

// populate candidate maps
structure.allCandidates().first().map {
  when (it) {
   is CommandCandidate -> {

      // update commandsByTrait
      // TODO refactor into generic function to eliminate duplicate code
      var commandsByTraitList: MutableList<CommandCandidate>? = commandsByTrait.get(it.trait.factory.traitId: String)
      if(commandsByTraitList == null) { commandsByTraitList = arrayListOf<CommandCandidate>() }

      commandsByTraitList.add(it)
      commandsByTrait.put(it.trait.factory.traitId, commandsByTraitList)

      // update commandsByDeviceType
      // TODO refactor into generic function to eliminate duplicate code
      for (t in it.types) {

      //TODO filter out device types not present in the home

      //TODO how to get a reference to device type id? 

      var commandsByDeviceTypeList: MutableList<CommandCandidate>? = commandsByDeviceType.get(t.factory.typeId: Id)

      if (commandsByDeviceTypeList == null)  { commandsByDeviceTypeList = arrayListOf<CommandCandidate>() }

      commandsByDeviceTypeList.add(it)

      commandsByDeviceType.put(t.factory.typeId, commandsByDeviceTypeList)
      }

      // update commandsByEntity
      // TODO refactor into generic function to eliminate duplicate code
     var commandsByEntityList: MutableList<CommandCandidate>? = commandsByEntity.get(it.entity.id: Id)
      if ( commandsByEntityList == null ) {commandsByEntityList = arrayListOf<CommandCandidate>()}
      commandsByEntityList.add(it)
      commandsByEntity.put(it.entity.id, commandsByEntityList)
   }

/*
   is StateReaderCandidate -> {

      // update stateReadersByTrait

      var stateReadersList: MutableList<StateReaderCandidate>? = stateReadersByTrait.get(it.trait.factory.traitId)
      if(stateReadersList == null) { stateReadersList = arrayListOf<StateReaderCandidate>() }

      stateReadersList.add(it)
      stateReadersByTrait.put(it.trait.factory.traitId, stateReadersList)

   // update stateReadersByDeviceType

      for (t in it.types) {
      //TODO filter out device types not present in the home
      var stateReadersList: MutableList<StateReaderCandidate>? = stateReadersByDeviceType.get(t.metadata.hashcode())

      if (stateReadersList == null)  { stateReadersList = arrayListOf<StateReaderCandidate>() }

      stateReadersList.put(it)

      stateReadersByDeviceType.put(t.metadata.hashCode(),deviceTypeStateReaderList)
      }

      // update stateReadersByEntity

      MutableList<StateReaderCandidate> entityStateReaderList? = stateReadersByEntity.get(it.entity.id)
      if  entityStateReaderList  is null  {entityStateReaderList = arrayListOf<StateReaderCandidate>()}
      entityStateReaderList.add(it)
      stateReadersByEntity.put(it.entity.id, entityStateReaderList)
   }

   */

   /*
   is StarterCandidate  -> {

      // update startersByTrait

      var startersList: MutableList<StateReaderCandidate>? = startersByTrait.get(it.trait.factory.traitId)
      if(startersList == null) { startersList = arrayListOf<StateReaderCandidate>() }

      startersList.add(it)
      startersByTrait.put(it.trait.factory.traitId, startersList)

   // update startersByDeviceType

      for (t in it.types) {
      //TODO filter out device types not present in the home
      var startersList: MutableList<StateReaderCandidate>? = startersByDeviceType.get(t.metadata.hashcode())

      if (startersList == null)  { startersList = arrayListOf<StateReaderCandidate>() }

      startersList.put(it)

      startersByDeviceType.put(t.metadata.hashCode(),deviceTypeStateReaderList)
      }

      // update startersByEntity

      MutableList<StateReaderCandidate> entityStateReaderList? = startersByEntity.get(it.entity.id)
      if  entityStateReaderList  is null  {entityStateReaderList = arrayListOf<StateReaderCandidate>()}
      entityStateReaderList.add(it)
      stateReadersByEntity.put(it.entity.id, entityStateReaderList)

      }
*/
 else -> println("unknown type encountered: " + it)

  }
}

자동화 실행

execute() 메서드를 사용하여 생성된 자동화를 실행합니다.

createdAutomation.execute()

자동화에 수동 시작 조건이 있는 경우 execute()는 수동 시작 조건 앞에 있는 모든 노드를 무시하고 해당 지점부터 자동화를 시작합니다. 자동화에 수동 시작 조건자가 없는 경우 첫 번째 시작 조건자 노드 다음 노드에서 실행이 시작됩니다.

execute() 작업이 실패하면 HomeException이 발생할 수 있습니다. 오류 처리를 참고하세요.

자동화 중지하기

stop() 메서드를 사용하여 실행 중인 자동화를 중지합니다.


createdAutomation.stop()

stop() 작업이 실패하면 HomeException이 발생할 수 있습니다. 오류 처리를 참고하세요.

구조물의 자동화 목록 가져오기

자동화는 구조 수준에서 정의됩니다. 구조의 automations()에서 수집하여 자동화의 Flow에 액세스합니다.


import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
structure.automations().collect {
  println("Available automations:")
  for (automation in it) {
    println(String.format("%S %S", "$automation.id", "$automation.name"))
  }
}

또는 로컬 Collection에 할당할 수도 있습니다.

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

var myAutomations: Collection<Automation> = emptyList()
myAutomations = structure.automations()

ID별 자동화 가져오기

자동화 ID별로 자동화를 가져오려면 구조에서 automations() 메서드를 호출하고 ID를 기준으로 일치시킵니다.

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()

응답:

// Here's how the automation looks like in the get response. Here, it's represented
// as if calling a println(automation.toString())

Automation(
  name = "automation-name",
  description = "automation-description",
  isActive = true,
  id = Id("automation@automation-id"),
  automationGraph = SequentialFlow(
    nodes = [
      Starter(
        entity="device@test-device",
        type="home.matter.0000.types.0101",
        trait="OnOff@6789..."),
      Action(
        entity="device@test-device",
        type="home.matter.0000.types.0101",
        trait="OnOff@8765...",
        command="on")
    ]))

이름으로 자동화 가져오기

Kotlin의 filter() 메서드를 사용하여 API 호출을 추가로 세분화할 수 있습니다. 이름으로 자동화를 가져오려면 구조의 자동화를 가져와 자동화 이름으로 필터링합니다.

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().filter {
  it.name.equals('Sunset Blinds') }

기기의 모든 자동화 가져오기

특정 기기를 참조하는 모든 자동화를 가져오려면 중첩된 필터링을 사용하여 각 자동화의 automationGraph를 스캔합니다.

import android.util.Log
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure
import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.Automation.automationGraph
import com.google.home.automation.Node
import com.google.home.automation.ParallelFlow
import com.google.home.automation.SelectFlow
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.automation.StateReader

...

fun collectDescendants(node: Node): List<Node> {
  val d: MutableList<Node> = mutableListOf(node)

  val children: List<Node> =
    when (node) {
      is SequentialFlow -> node.nodes
      is ParallelFlow -> node.nodes
      is SelectFlow -> node.nodes
      else -> emptyList()
    }
  for (c in children) {
    d += collectDescendants(c)
  }
  return d
}

val myDeviceId = "device@452f78ce8-0143-84a-7e32-1d99ab54c83a"
val structure = homeManager.structures().list().single()
val automations =
  structure.automations().first().filter {
    automation: Automation ->
    collectDescendants(automation.automationGraph!!).any { node: Node ->
      when (node) {
        is Starter -> node.entity.id.id == myDeviceId
        is StateReader -> node.entity.id.id == myDeviceId
        is Action -> node.entity.id.id == myDeviceId
        else -> false
      }
    }
  }

자동화 업데이트

자동화의 메타데이터를 업데이트하려면 update() 메서드를 호출하여 메타데이터를 설정하는 람다 표현식을 전달합니다.

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()
automation.update { this.name = "Flashing lights 2" }

update() 메서드는 자동화 그래프를 완전히 대체하는 기능을 지원하지만 그래프의 노드별 수정은 지원하지 않습니다. 노드 간 상호의존성으로 인해 노드별 수정은 오류가 발생하기 쉽습니다. 자동화 로직을 변경하려면 새 그래프를 생성하고 기존 그래프를 완전히 대체하세요.

자동화 삭제

자동화를 삭제하려면 구조의 deleteAutomation() 메서드를 사용합니다. 자동화는 ID를 사용하여 삭제해야 합니다.

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().first()
structure.deleteAutomation(automation.id)

삭제에 실패하면 HomeException이 발생할 수 있습니다. 오류 처리를 참고하세요.

자동화에 미치는 기기 삭제의 영향

사용자가 자동화에 사용되는 기기를 삭제하면 삭제된 기기는 시작 조건을 트리거할 수 없으며 자동화에서 기기의 속성을 읽거나 기기에 명령을 실행할 수 없습니다. 예를 들어 사용자가 홈에서 OccupancySensorDevice를 삭제했는데 자동화에 OccupancySensorDevice에 종속된 시작 조건이 있는 경우 해당 시작 조건은 더 이상 자동화를 활성화할 수 없습니다.