在 Android 上建構自動化動作

您可以透過 Android 適用的 Home API 存取 Automation 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"))!!

接著,使用 Automation DSL 定義自動化動作的邏輯。在 Home API 中,自動化動作由 Automation 介面表示。這個介面包含一組屬性:

  • 中繼資料,例如名稱和說明。
  • 旗標,例如指出是否可以執行自動化動作。
  • 包含自動化動作邏輯的節點清單,稱為自動化動作圖,由 automationGraph 屬性表示。

根據預設,automationGraph 的類型為 SequentialFlow,這個類別包含依序執行的節點清單。每個節點代表自動化動作的一個元素,例如啟動條件、限制條件或動作。

為自動化動作指派name和description。

建立自動化動作時,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 參考資料。

即使 createAutomation() 完成時未擲回例外狀況,建立的自動化動作也可能無效或無法執行。後端允許儲存無效的自動化動作草稿 (例如使用者未授予必要同意聲明,像是熟面孔偵測,或裝置功能不完整)。

請務必驗證 isValid,並檢查傳回的 Automation 執行個體上的 validationIssues:

val createdAutomation = structure.createAutomation(automation)

if (!createdAutomation.isValid) {
  // Iterate through validation issues to identify errors and warnings
  for (issue in createdAutomation.validationIssues) {
    when (issue.severity) {
      ValidationIssueSeverity.ERROR -> {
        Log.e(
          "Automation",
          "Validation error on node ${issue.node}: ${issue.issueType}"
        )
        // Handle error (for example, prompt the user to enable missing
        // consents or device features)
      }
      ValidationIssueSeverity.WARNING -> {
        Log.w(
          "Automation",
          "Validation warning on node ${issue.node}: ${issue.issueType}"
        )
      }
      else -> {}
    }
  }
}

程式碼範例

以下提供一些程式碼範例,可用於實作「在 Android 上設計自動化動作」頁面所述的部分假設自動化動作。

簡單的自動化動作

在上午 8:00 升起百葉窗的自動化動作 可能像這樣實作:

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

使用實體篩選器動態選取裝置

編寫自動化動作時,不一定要指定特定裝置。實體篩選器功能可讓自動化動作在執行階段根據各種條件選取裝置。

舉例來說,使用實體篩選條件時,自動化動作可以指定:

  • 特定裝置類型的所有裝置
  • 特定房間內的所有裝置
  • 特定房間內特定類型的所有裝置
  • 所有已開啟的裝置
  • 特定房間內所有已開啟的裝置

如要使用實體篩選器,請按照下列步驟操作:

  1. 在 Structure 或 Room 上呼叫 atExecutionTime()。這會傳回 TypedExpression<TypedEntity<StructureType>>。
  2. 在這個物件上,呼叫 getDevicesOfType(),並傳遞 DeviceType。

實體篩選器可用於啟動條件、狀態讀取器和動作。

舉例來說,如要讓任何開關燈具透過啟動條件觸發自動化動作,請按照下列步驟操作:

// If any light is turned on or off
val starter =
  starter(
    entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )

如要在狀態讀取器中擷取結構中所有燈具 (特別是開/關燈具) 的 OnOff 狀態,請按照下列步驟操作:

// Build a Map<Entity, OnOff>
val onOffStateOfAllLights =
  stateReader(
    entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )

如要取得特定房間的燈具,並在條件中使用:

val livingRoomLights =
  stateReader(
    entityExpression = livingRoom.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )
// Are any of the lights in the living room on?
condition { expression = livingRoomLights.values.any { it.onOff equals true } }

執行階段:

情境 結果
沒有任何裝置符合啟動條件。 自動化動作未觸發。
沒有任何裝置符合狀態讀取器的條件。 自動化動作會啟動,但會根據條件節點繼續執行。
沒有任何裝置符合動作條件。 自動化動作已啟動,但沒有任何反應。

以下範例是自動化動作,只要關閉個別燈具,就會關閉除了走廊燈以外的所有燈具:

val unused = automation {
  sequential {
    // If any light is turned on or off
    val starter =
      starter(
        entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
        trait = OnOff,
      )
    condition {
      // Check to see if the triggering light was turned off
      expression = starter.onOff equals false
    }
    // Turn off all lights except the hall light
    action(
      entityExpression =
        structure.atExecutionTime().getDevicesOfType(OnOffLightDevice).filter {
          it notEquals entity(hallwayLight, OnOffLightDevice)
        }
    ) {
      command(OnOff.on())
    }
  }
}

執行自動化動作

使用 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() 方法,並傳遞設定中繼資料的 lambda 運算式:

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() 方法支援完全取代自動化圖表,但不支援逐一編輯圖表節點。節點之間相互依賴,因此逐一編輯節點容易出錯。如要變更自動化動作的邏輯,請產生新的圖表,並完全取代現有圖表。

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: Automation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()
automation.update {
  this.automationGraph = sequential {
    val laundryWasherCompletionEvent =
      starter<_>(laundryWasher, LaundryWasherDevice, OperationCompletionEvent)
    condition {
      expression =
        laundryWasherCompletionEvent.completionErrorCode equals
          // UByte 0x00u means NoError
          0x00u
    }
    action(speaker, SpeakerDevice) { command(AssistantBroadcast.broadcast("laundry is done")) }
    }
  }
}

刪除自動化動作

如要刪除自動化動作,請使用結構體的 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,該啟動條件就無法再啟動自動化動作。