利用设备发现

Discovery API 旨在供可根据用户住宅中存在的设备创建自动化操作的应用使用。它可以在运行时向应用显示给定结构中存在的 trait 和设备,以便在自动化操作中使用。此外,它还会显示关联的命令、属性和事件,以及参数和字段允许的值范围。

Discovery API 会忽略结构中存在的任何不受 Automation API 支持的设备或特征,以及未在 FactoryRegistry 中注册的任何设备或特征。如需详细了解如何使用 FactoryRegistry,请参阅创建 Home 实例

使用 API

Discovery API 的核心是 HasCandidates 接口,它是包含 StructureRoomHomeDevice 的类型层次结构的根。

HasCandidates 接口定义了两个方法:candidates()allCandidates(),它们都返回 Flow 对象。

  • candidates() 会为实体生成自动化操作候选项列表 (StructureRoomHomeDevice)。

  • allCandidates() 会为实体及其所有子项生成自动化操作候选项列表。Room 不支持此方法。

与 Home API 返回的其他 Flow 对象不同,这些对象包含一次性快照。

如需获取最新的可用候选对象列表,开发者必须每次都调用 candidates()allCandidates(),而不能仅对 Flow 对象调用 collect()。此外,由于这两种方法特别耗资源,因此如果调用频率高于每分钟一次,系统将返回缓存的数据,而这些数据可能并不反映当时的实际状态。

NodeCandidate 接口表示这两种方法找到的候选节点,是包含以下接口的层次结构的根:

以及以下类:

使用自动化操作候选项

假设您正在编写一款应用,该应用会创建一项自动化操作,以便在用户指定的时间关闭一组智能百叶窗。不过,您不知道用户是否拥有支持 WindowCovering trait 的设备,以及 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,您可以了解某个 trait 缺少使用前提条件,例如订阅或结构体地址。它使用 Candidate 类的 unsupportedReasons 属性执行此操作。在 candidates() 调用期间,此属性会填充 UnsupportedCandidateReason。调用 createAutomation() 时,验证错误消息中也会显示相同的信息。

原因示例:

例如,如需处理 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 lowerBoundupperBoundstepunit
NumberSetConstraint allowedSetunit
StringConstraint allowedSetdisallowedSetisCaseSensitivemaxLengthminLengthregex
StructConstraint fieldConstraints
ListConstraint elementConstraint

使用约束条件

假设您正在编写一款应用,该应用会创建一个自动化操作,用于使用 LevelControl trait 设置设备的音量。以下示例展示了如何确保用于设置 LevelControl trait 的 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() 方法确定每部设备是否支持自动化操作所需的所有 trait。
  3. 使用 supports() 方法确定每个 trait 是否支持自动化操作所需的所有属性命令

请务必注意,如果您使用 Device API 进行发现,则无法获享以下 Discovery API 功能:

  • 自动滤除 Automation API 不支持的特征。
  • 能够为用户提供一个选项,以便为使用约束条件的属性和参数选择有效值。