Cómo controlar dispositivos en Android

Cómo verificar si un atributo admite un comando

También se puede verificar la compatibilidad con un comando de rasgo. También usa la función supports a nivel de atributo para verificar si un comando es compatible con un dispositivo en particular.

Por ejemplo, para verificar si un dispositivo admite el comando toggle de la característica On/Off, haz lo siguiente:

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

Enviar un comando a un dispositivo

Enviar un comando es similar a leer un atributo de estado de un atributo. Para encender o apagar el dispositivo, usa el comando Toggle del atributo OnOff, que se define en el modelo de datos del ecosistema de Google Home como toggle(). Este método cambia onOff a false si es true, o a true si es false:

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

Todos los comandos de rasgos son funciones suspend y solo se completan cuando la API devuelve una respuesta (como confirmar que cambió el estado del dispositivo). Los comandos pueden mostrar una excepción si se detecta un problema con el flujo de ejecución. Como desarrollador, debes usar un bloque try-catch para controlar estas excepciones de forma adecuada y mostrar información detallada a los usuarios en los casos en que los errores se puedan corregir. Las excepciones no controladas detendrán el tiempo de ejecución de la app y pueden provocar fallas en ella.

Como alternativa, usa los comandos off() o on() para establecer el estado de forma explícita:

onOffTrait.off()
onOffTrait.on()

Después de enviar un comando para cambiar el estado, una vez que se complete, puedes leerlo como se describe en Cómo leer el estado de un dispositivo para controlarlo en tu app. Como alternativa, usa flujos como se describe en Cómo observar el estado, que es el método preferido.

Envía un comando con parámetros

Algunos comandos pueden usar parámetros, como los de los atributos OnOff o 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(),
)

Algunos comandos tienen argumentos opcionales, que se ubican después de los argumentos obligatorios.

Por ejemplo, el comando step para el atributo FanControl tiene dos argumentos opcionales:

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 }

Verifica si un atributo admite un rasgo

Es posible que algunos dispositivos admitan un atributo Matter, pero no un atributo específico. Por ejemplo, un dispositivo Cloud-to-cloud que se asignó a Matter podría no admitir todos los atributos de Matter. Para controlar casos como estos, usa la función supports a nivel de rasgo y la enumeración Attribute del rasgo para verificar si el atributo es compatible con un dispositivo en particular.

Por ejemplo, para verificar si un dispositivo admite el atributo onOff de la característica On/Off, haz lo siguiente:

// 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!")
}

Algunos atributos pueden ser nulos en la especificación de Matter o en el esquema de smart home de Cloud-to-cloud. Para estos atributos, puedes determinar si un valor nulo que devuelve el atributo se debe a que el dispositivo no informa ese valor o si el valor del atributo en realidad es null, con isNullable además de supports:

// 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!")
}

Actualiza los atributos de los rasgos

Si quieres cambiar el valor de un atributo determinado y ninguno de los comandos del rasgo lo hace, es posible que el atributo admita que se establezca su valor de forma explícita.

La posibilidad de cambiar el valor de un atributo depende de dos factores:

  • ¿Se puede escribir en el atributo?
  • ¿Puede cambiar el valor del atributo como un efecto secundario de enviar un comando de rasgo?

La documentación de referencia de los rasgos y sus atributos proporciona esta información.

Por lo tanto, las combinaciones de propiedades que dictan cómo se puede cambiar el valor de un atributo son las siguientes:

  • Es de solo lectura y no se ve afectado por otros comandos. Esto significa que el valor del atributo no cambia. Por ejemplo, el atributo currentPosition de la característica Switch.

  • Es de solo lectura y se ve afectado por otros comandos. Esto significa que la única forma en que el valor del atributo puede cambiar es como resultado del envío de un comando. Por ejemplo, el atributo currentLevel de la característica LevelControl Matter es de solo lectura, pero su valor se puede mutar con comandos como moveToLevel.

  • Se puede escribir en él y no se ve afectado por otros comandos. Esto significa que puedes cambiar directamente el valor del atributo con la función update de la característica, pero no hay comandos que afecten el valor del atributo. Por ejemplo, el atributo WrongCodeEntryLimit de la característica DoorLock.

  • Se puede escribir en él y se ve afectado por otros comandos. Esto significa que puedes cambiar directamente el valor del atributo con la función update del rasgo, y el valor del atributo puede cambiar como resultado del envío de un comando. Por ejemplo, el atributo speedSetting de FanControlTrait se puede escribir directamente, pero también se puede mutar con el comando step.

Ejemplo de uso de la función de actualización para cambiar el valor de un atributo

En este ejemplo, se muestra cómo establecer de forma explícita el valor del atributo DoorLockTrait.WrongCodeEntryLimit.

Para establecer el valor de un atributo, llama a la función update del rasgo y pásale una función mutadora que establezca el valor nuevo. Se recomienda verificar primero que el atributo sea compatible con el rasgo.

Por ejemplo:

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

Envía varios comandos a la vez

La API de Batching permite que un cliente envíe varios comandos de dispositivo de las APIs de Home en una sola carga útil. Los comandos se agrupan en una sola carga útil y se ejecutan en paralelo, de manera similar a como se podría construir una automatización de la API de Home con el nodo paralelo, como en el ejemplo Abrir las persianas antes del amanecer. Sin embargo, la API de Batching permite comportamientos más complejos y sofisticados que la API de Automation, como la capacidad de seleccionar dispositivos de forma dinámica en el entorno de ejecución según cualquier criterio.

Los comandos de un lote pueden orientarse a varios rasgos en varios dispositivos, en varias habitaciones y en varias estructuras.

El envío de comandos en un lote permite que los dispositivos realicen acciones de forma simultánea, lo que no es posible cuando los comandos se envían de forma secuencial en solicitudes independientes. El comportamiento que se logra con los comandos por lotes permite que el desarrollador establezca el estado de un grupo de dispositivos para que coincida con un estado agregado predeterminado.

Usa la API de Batching

La invocación de comandos a través de la API de procesamiento por lotes implica tres pasos básicos:

  1. Invoca el método Home.sendBatchedCommands().
  2. Dentro del cuerpo del bloque sendBatchedCommands(), especifica los comandos que se incluirán en el lote.
  3. Verifica los resultados de los comandos enviados para ver si se ejecutaron correctamente o fallaron.

Envía comandos por lotes

Realiza una llamada al método Home.sendBatchedCommands(). Tras bambalinas, este método configura una expresión lambda en un contexto de lote especial.

home.sendBatchedCommands() {

Especifica comandos por lotes

Dentro del cuerpo del bloque sendBatchedCommands(), propaga los comandos por lotes. Los comandos por lotes son versiones "shadow" de los comandos existentes de la API de Device que se pueden usar en un contexto por lotes y se nombran con el sufijo agregado Batchable. Por ejemplo, el comando moveToLevel() de la característica LevelControl tiene una contraparte llamada moveToLevelBatchable().

Ejemplo:

  val response1 = add(command1)

  val response2 = add(command2)

El lote se envía automáticamente una vez que se agregaron todos los comandos al contexto del lote y la ejecución salió del contexto.

Las respuestas se capturan en objetos DeferredResponse<T>.

Las instancias de DeferredResponse<T> se pueden recopilar en un objeto de cualquier tipo, como un Collection o una clase de datos que definas. Cualquiera sea el tipo de objeto que elijas para ensamblar las respuestas, es lo que muestra sendBatchedCommands(). Por ejemplo, el contexto por lotes puede mostrar dos instancias de DeferredResponse en un Pair:

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

Como alternativa, el contexto por lotes puede mostrar las instancias de DeferredResponse en una clase de datos personalizada:

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

Verifica cada respuesta

Fuera del bloque sendBatchedCommands(), verifica las respuestas para determinar si el comando correspondiente se ejecutó correctamente o falló. Para ello, se llama a DeferredResponse.getOrThrow(), que hace lo siguiente: - Devuelve el resultado del comando ejecutado. - O bien, si el alcance del lote no se completó o el comando no se ejecutó correctamente, arroja un error.

Solo debes verificar los resultados fuera del alcance de la lambda sendBatchedCommands().

Ejemplo

Supongamos que quieres compilar una app que use la API de Batching para configurar una escena de "buenas noches" que configure todos los dispositivos de la casa para la noche, cuando todos estén durmiendo. Esta app debería apagar las luces y trabar las puertas delantera y trasera.

Esta es una forma de abordar la tarea:

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

Controla un grupo de dispositivos

Además de tener un control detallado sobre cada miembro de un grupo mediante la iteración por sus dispositivos miembros, puedes controlar un DeviceGroup como una sola entidad. DeviceGroup implementa la interfaz PartsProvider, que expone las partes DeviceType que admiten los dispositivos del grupo:

  • group.parts() devuelve un Flow<Set<DeviceType>> de todos los tipos de dispositivos presentes en el grupo.
  • group.part() y group.partOrNull() devuelven un flujo para un tipo de dispositivo específico, como OnOffLightDevice o DimmableLightDevice.

Cuando invocas un comando de atributo en un tipo de dispositivo que forma parte de un DeviceGroup, el SDK retransmite el comando a todos los dispositivos miembros asociados del grupo que admiten esa capacidad, independientemente de su conectividad o transporte subyacentes (como Matter o Cloud-to-cloud).

Atributos de rasgos de partes de grupos y manejo de errores

La segmentación de una parte del grupo simplifica los casos comunes en los que deseas enviar un comando idempotente (como encender o apagar las luces, o establecer un nivel de brillo) en un grupo. Cuando trabajes con rasgos y comandos en una parte de DeviceGroup, ten en cuenta los siguientes comportamientos:

  • Los atributos de estado dinámicos (como el atributo onOff del rasgo OnOff o el atributo currentLevel del rasgo LevelControl) no suelen propagarse en el rasgo de una parte del grupo porque los dispositivos miembros de un grupo pueden estar en diferentes estados que no se pueden combinar en un solo valor. Sin embargo, los comandos admitidos (verificados con supports(), como LevelControl.Command.MoveToLevelWithOnOff en una luz regulable) y los atributos de capacidad seleccionados (como colorTempPhysicalMinMireds y colorTempPhysicalMaxMireds en ColorControl para luces de color) se propagan para que tu app pueda configurar los controles de la IU de forma adecuada.
  • No todos los comandos de los atributos de los miembros del grupo están disponibles en una parte del grupo. En particular, no están disponibles los comandos que devuelven valores.
  • Un comando en una parte de grupo no devuelve resultados individuales para cada comando de dispositivo miembro subyacente. Para inspeccionar los resultados de comandos individuales o llamar a comandos que devuelven valores, itera directamente a través de los dispositivos miembros del grupo.
  • Los comandos que se envían a una parte en un DeviceGroup se completan una vez que finaliza toda la distribución en los dispositivos miembros y se recopilan todos los éxitos o fallas de los comandos subyacentes. Un comando solo falla (arroja una HomeException) si falla para cada dispositivo miembro asociado o si todos los dispositivos de destino no están disponibles, lo que se muestra como una falla de un solo comando que coincide con la sintaxis de llamada de un solo comando. Si al menos un dispositivo miembro ejecuta el comando, el comando de grupo se completa sin arrojar una excepción.

Cómo enviar comandos a una parte de un grupo

Para encender todas las luces OnOffLightDevice de un grupo de dispositivos, di lo siguiente:

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

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

También puedes inspeccionar todos los tipos de dispositivos presentes en un grupo con group.parts() para segmentar un atributo en varios tipos de dispositivos relacionados (por ejemplo, apagar cualquier tipo de dispositivo de luz del grupo que admita 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()

Puedes verificar la compatibilidad de los comandos o leer los límites de capacidad de una parte del grupo antes de enviar un comando (por ejemplo, verificar MoveToLevelWithOnOff en una parte de luz regulable o leer colorTempPhysicalMinMireds y colorTempPhysicalMaxMireds en una parte de luz de temperatura de color):

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