在 Android 上构建自动化操作

Automation API 可通过 Android 版 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"))!!

然后,使用 Automation DSL 定义自动化操作的逻辑。在 Home API 中,自动化操作由 Automation 接口表示。此接口包含一组属性:

  • 元数据,例如名称和说明。
  • 标志,用于指示自动化操作是否可以执行等。
  • 包含自动化操作逻辑的节点列表,称为自动化操作图,由 automationGraph 属性表示。

automationGraph默认类型为 SequentialFlow,这是一个包含按顺序执行的节点列表的类。每个节点都代表自动化操作的一个元素,例如启动方式、条件或操作。

为自动化操作分配 namedescription

创建自动化操作时,isActive 标志默认设置为 true,因此无需显式设置此标志,除非您最初希望停用自动化操作。在这种情况下,请在创建期间将该标志设置为 false

DraftAutomation 接口用于构建和创建自动化操作, 而 Automation 接口用于检索。例如,以下是自动化操作的 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参考文档

代码示例

下面我们提供了一些示例代码,这些代码可用于实现 “在 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. StructureRoom 上,调用 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")
    ]))

按名称获取自动化操作

filter() Kotlin 中的 方法可用于进一步优化 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 的启动方式,则该启动方式将无法再激活自动化操作。