在 Android 设备上控制设备

检查特征是否支持命令

您还可以检查是否支持某个特征命令。此外,您还可以使用 trait 级 supports 函数来检查特定设备是否支持某个命令。

例如,如需检查设备是否支持 On/Off 特性的 toggle 命令,请运行以下命令:

// Check if the OnOff trait supports the toggle command.
if (onOffTrait.supports(OnOff.Command.Toggle)) {
  println("onOffTrait supports toggle command")
} else {
  println("onOffTrait does not support stateful toggle command")
}

向设备发送命令

发送命令与从 trait 读取状态属性类似。如需开启或关闭设备,请使用 OnOff 特性的 Toggle 命令,该命令在 Google Home 生态系统数据模型中定义为 toggle()。此方法会将 onOff 更改为 false(如果其值为 true),或更改为 true(如果其值为 false):

// Calling a command on a trait.
try {
  onOffTrait.toggle()
} catch (e: HomeException) {
  // Code for handling the exception
}

所有 trait 命令都是 suspend 函数,只有在 API 返回响应(例如确认设备状态已更改)时才会完成。如果检测到执行流程存在问题,命令可能会返回异常。作为开发者,您应使用 try-catch 代码块来妥善处理这些异常,并在错误可操作的情况下向用户显示详细信息。未处理的异常会停止应用运行时,并可能导致应用崩溃。

或者,使用 off() 或 on() 命令明确设置状态:

onOffTrait.off()
onOffTrait.on()

发送更改状态的命令后,一旦命令完成,您就可以按照读取设备状态中的说明读取状态,以便在应用中进行处理。或者,您也可以按照观察状态中的说明使用 flow,这是首选方法。

发送带参数的命令

某些命令可能会使用参数,例如 OnOff 或 LevelControl 特性中的参数:

offWithEffect

// Turn off the light using the DyingLight effect.
onOffTrait.offWithEffect(
  effectIdentifier = OnOffTrait.EffectIdentifierEnum.DyingLight,
  effectVariant = 0u,
)

moveToLevel

// Change the brightness of the light to 50%
levelControlTrait.moveToLevel(
  level = 127u.toUByte(),
  transitionTime = null,
  optionsMask = LevelControlTrait.OptionsBitmap(),
  optionsOverride = LevelControlTrait.OptionsBitmap(),
)

有些命令具有可选参数,这些参数位于必需参数之后。

例如,FanControl 特性的 step 命令有两个可选参数:

val fanControlTraitFlow: Flow<FanControl?> =
  device.type(FanDevice).map { it.standardTraits.fanControl }.distinctUntilChanged()

val fanControl = fanControlTraitFlow.firstOrNull()

// Calling a command with optional parameters not set.
fanControl?.step(direction = FanControlTrait.StepDirectionEnum.Increase)

// Calling a command with optional parameters.
fanControl?.step(direction = FanControlTrait.StepDirectionEnum.Increase) { wrap = true }

检查特征是否支持属性

某些设备可能支持 Matter 特征,但不支持特定属性。例如,映射到 Matter 的 Cloud-to-cloud 设备可能不支持每个 Matter 属性。如需处理此类情况,请使用 trait 级 supports 函数和 trait 的 Attribute 枚举来检查特定设备是否支持该属性。

例如,如需检查设备是否支持 On/Off 特性的 onOff 属性,请执行以下操作:

// Check if the OnOff trait supports the onOff attribute.
if (onOffTrait.supports(OnOff.Attribute.onOff)) {
  println("onOffTrait supports onOff state")
} else {
  println("onOffTrait is for a command only device!")
}

某些属性在 Matter 规范或 Cloud-to-cloud smart home 架构中可为 null。对于这些属性,除了 supports 之外,您还可以使用 isNullable 来确定属性返回的 null 是因为设备未报告该值,还是因为属性的值实际上是 null:

// Check if a nullable attribute is set or is not supported.
if (onOffTrait.supports(OnOff.Attribute.startUpOnOff)) {
  // The device supports startupOnOff, it is safe to expect this value in the trait.
  if (OnOff.Attribute.startUpOnOff.isNullable && onOffTrait.startUpOnOff == null) {
    // This value is nullable and set to null. Check the specification as to
    // what null in this case means
    println("onOffTrait supports startUpOnOff and it is null")
  } else {
    // This value is nullable and set to a value.
    println("onOffTrait supports startUpOnOff and it is set to ${onOffTrait.startUpOnOff}")
  }
} else {
  println("onOffTrait does not support startUpOnOff!")
}

更新特征属性

如果您想更改给定属性的值,但 trait 的命令都无法做到这一点,则该属性可能支持显式设置其值。

属性的值是否可以更改取决于两个因素:

  • 属性是否可写?
  • 属性值是否会因发送特征命令而产生副作用?

有关特征及其属性的参考文档提供了这些信息。

因此,决定属性值如何更改的属性组合如下:

使用更新函数更改属性值的示例

此示例展示了如何明确设置 DoorLockTrait.WrongCodeEntryLimit 属性的值。

如需设置属性值,请调用 trait 的 update 函数,并向其传递一个用于设置新值的 mutator 函数。 最好先验证 trait 是否支持某个属性。

例如:

    val doorLockDevice = home.devices().list().first { device -> device.has(DoorLock) }

    val traitFlow: Flow<DoorLock?> =
      doorLockDevice.type(DoorLockDevice).map { it.standardTraits.doorLock }.distinctUntilChanged()

    val doorLockTrait: DoorLock = traitFlow.first()!!

    if (doorLockTrait.supports(DoorLock.Attribute.wrongCodeEntryLimit)) {
      val unused = doorLockTrait.update { setWrongCodeEntryLimit(3u) }
    }

一次发送多条命令

借助 Batching API,客户端可以在单个载荷中发送多个 Home API 设备命令。这些命令会批量处理成单个载荷并并行执行,类似于使用并行节点构建 Home API 自动化的方式,例如在日出前打开百叶窗示例。不过,与 Automation API 相比,Batching API 支持更复杂、更高级的行为,例如能够在运行时根据任何条件动态选择设备。

一个批次中的命令可以针对多个结构、多个房间、多个设备中的多个 trait。

批量发送命令可让设备同时执行操作,而当命令在单独的请求中按顺序发送时,这实际上是不可能的。使用批处理命令实现的行为允许开发者设置一组设备的状态,以匹配预定的汇总状态。

使用 Batching API

通过 Batching API 调用命令涉及三个基本步骤:

  1. 调用 Home.sendBatchedCommands() 方法。
  2. 在 sendBatchedCommands() 代码块的正文中,指定要包含在批处理中的命令。
  3. 检查已发送命令的结果,查看其是成功还是失败。

发送批量命令

调用 Home.sendBatchedCommands() 方法。在后台,此方法会在特殊的批处理上下文中设置 lambda 表达式。

home.sendBatchedCommands() {

指定批处理命令

在 sendBatchedCommands() 块的正文中,填充可批处理的命令。可批处理命令是现有 Device API 命令的“影子”版本,可在批处理上下文中使用,并以添加的后缀 Batchable 命名。例如,LevelControl trait 的 moveToLevel() 命令有一个名为 moveToLevelBatchable() 的对应命令。

示例:

  val response1 = add(command1)

  val response2 = add(command2)

将所有命令添加到批处理上下文且执行离开上下文后,系统会自动发送批处理。

响应会捕获到 DeferredResponse<T> 对象中。

DeferredResponse<T> 实例可以收集到任何类型的对象中,例如 Collection 或您定义的数据类。无论您选择哪种类型的对象来汇编响应,sendBatchedCommands() 都会返回该对象。例如,批处理上下文可以在 Pair 中返回两个 DeferredResponse 实例:

  val (response1, response2) = homeClient.sendBatchedComamnds {
    val response1 = add(someCommandBatched(...))
    val response2 = add(someOtherCommandBatched(...))
    Pair(response1, response2)
  }

或者,批处理上下文可以返回自定义数据类中的 DeferredResponse 实例:

  // Custom data class
  data class SpecialResponseHolder(
    val response1: DeferredResponse<String>,
    val response2: DeferredResponse<Int>,
    val other: OtherResponses
  )
  data class OtherResponses(...)

检查每个回答

在 sendBatchedCommands() 代码块外部,检查响应以确定相应命令是成功还是失败。为此,您可以调用 DeferredResponse.getOrThrow(),该方法会: - 返回已执行命令的结果, - 或者,如果批处理范围尚未完成或命令不成功,则会抛出错误。

您应该仅在 sendBatchedCommands() lambda 范围之外检查结果。

示例

假设您想构建一款应用,该应用使用 Batching API 设置“晚安”场景,以便在每个人都入睡时为住宅中的所有设备配置夜间模式。此应用应该会关灯并锁上前门和后门。

以下是完成此任务的一种方法:

val lightDevices: List<OnOffLightDevice>
val doorlockDevices: List<DoorLockDevice>

// Send all the commands
val responses: List<DeferredResponse<Unit>> = home.sendBatchedCommands {
  // For each light device, send a Batchable command to turn it on
  val lightResponses: List<DeferredResponse<Unit>> = lightDevices.map { lightDevice ->
    add(lightDevice.standardTraits.onOff.onBatchable())
  }

  // For each doorlock device, send a Batchable command to lock it
  val doorLockResponse: List<DeferredResponse<Unit>> = doorlockDevices.map { doorlockDevice ->
    add(doorlockDevice.standardTraits.doorLock.lockDoorBatchable())
  }

  lightResponses + doorLockResponses
}

// Check that all responses were successful
for (response in responses) {
  response.getOrThrow()
}

控制设备组

除了通过遍历其成员设备来精细控制组中的每个成员之外,您还可以将 DeviceGroup 作为一个实体进行控制。DeviceGroup 会实现 PartsProvider 接口,该接口会公开群组中设备支持的 DeviceType 部分:

  • group.parts() 会返回一个 Flow<Set<DeviceType>>,其中包含该组中存在的所有设备类型。
  • group.part() 和 group.partOrNull() 会返回特定设备类型(例如 OnOffLightDevice 或 DimmableLightDevice)的流。

当您对 DeviceGroup 中的设备类型调用 trait 命令时,SDK 会将该命令中继到组中支持该功能的所有关联成员设备,无论其底层连接或传输方式(例如 Matter 或 Cloud-to-cloud)如何。

组部分特征属性和错误处理

定位到群组部分可简化您想要在群组中发送幂等命令(例如开灯或关灯,或设置亮度级别)的常见情况。在 DeviceGroup 部分使用 trait 和命令时,请注意以下行为:

  • 动态状态属性(例如 OnOff 特性的 onOff 属性或 LevelControl 特性的 currentLevel 属性)通常不会在群组部分的特性中填充,因为群组中的成员设备可能处于无法合并为单个值的不同状态。不过,系统会填充支持的命令(通过 supports() 检查,例如可调光灯上的 LevelControl.Command.MoveToLevelWithOnOff)和部分功能属性(例如彩色灯上 ColorControl 的 colorTempPhysicalMinMireds 和 colorTempPhysicalMaxMireds),以便您的应用可以适当地配置界面控件。
  • 并非群组成员特征的所有命令都适用于群组部分。 尤其是,返回值的命令不可用。
  • 对群组部分执行的命令不会返回每个底层成员设备命令的单独结果。如需检查单个命令结果或调用返回值的命令,请直接遍历群组的成员设备。
  • 发送到 DeviceGroup 上的某个部分的命令会在跨成员设备的整个扇出完成后完成,并且系统会收集所有底层命令的成功或失败情况。只有在命令针对每个关联的成员设备都失败或所有目标设备都不可用时,命令才会失败(抛出 HomeException),并显示为与单命令调用语法匹配的单命令失败。如果至少有一个成员设备执行了该命令,则群组命令会完成,而不会抛出异常。

将命令发送至群组部分

如需开启设备组中的所有 OnOffLightDevice 灯,请执行以下操作:

val lightPart = group.partOrNull(OnOffLightDevice).firstOrNull()
val onOffTrait = lightPart?.trait(OnOff)

if (onOffTrait?.supports(OnOff.Command.On) == true) {
  onOffTrait.on()
}

您还可以使用 group.parts() 检查群组中存在的所有设备类型,以便针对多种相关设备类型定位某个特征(例如,关闭群组中支持 OnOff 的任何灯具设备类型):

val lightTypes = listOf(
  OnOffLightDevice,
  DimmableLightDevice,
  ColorTemperatureLightDevice,
  ExtendedColorLightDevice,
)

val parts = group.parts().first()
val onOffTrait = lightTypes.firstNotNullOfOrNull { type ->
  parts.firstOrNull { it.factory == type }
}?.trait(OnOff)

onOffTrait?.off()

您可以在发送命令之前检查命令支持情况或读取群组部件的功能边界(例如,检查可调光灯部件上的 MoveToLevelWithOnOff,或读取色温灯部件上的 colorTempPhysicalMinMireds 和 colorTempPhysicalMaxMireds):

val dimmableLight = group.partOrNull(DimmableLightDevice).firstOrNull()
val levelControl = dimmableLight?.trait(LevelControl)

if (
  levelControl?.supports(
    LevelControl.Command.MoveToLevelWithOnOff
  ) == true
) {
  levelControl.moveToLevelWithOnOff(
    level = 127u,
    transitionTime = null,
    optionsMask = LevelControlTrait.OptionsBitmap(),
    optionsOverride = LevelControlTrait.OptionsBitmap(),
  )
}

val colorLight = group.partOrNull(ColorTemperatureLightDevice).firstOrNull()
val colorControl = colorLight?.trait(ColorControl)
val minMireds = colorControl?.colorTempPhysicalMinMireds
val maxMireds = colorControl?.colorTempPhysicalMaxMireds

if (colorControl != null && minMireds != null && maxMireds != null) {
  val midMireds = ((minMireds.toInt() + maxMireds.toInt()) / 2).toUShort()
  colorControl.moveToColorTemperature(
    colorTemperatureMireds = midMireds,
    transitionTime = 0u,
    optionsMask = ColorControlTrait.OptionsBitmap(),
    optionsOverride = ColorControlTrait.OptionsBitmap(),
  )
}