Controlar dispositivos no Android

Verificar se um atributo é compatível com um comando

Também é possível verificar a compatibilidade de um comando de trait. Use também a função supports no nível do atributo para verificar se um comando é compatível com um dispositivo específico.

Por exemplo, para verificar se um dispositivo é compatível com o comando toggle do atributo On/Off:

// 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 um comando para um dispositivo

Enviar um comando é semelhante a ler um atributo de estado de um trait. Para ligar ou desligar o dispositivo, use o comando Toggle da característica OnOff, definido no modelo de dados do ecossistema do Google Home como toggle(). Esse método muda onOff para false se for true ou para true se for false:

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

Todos os comandos de trait são funções suspend e só são concluídos quando uma resposta é retornada pela API (como a confirmação de que o estado do dispositivo mudou). Os comandos podem retornar uma exceção se um problema for detectado no fluxo de execução. Como desenvolvedor, use um bloco try-catch para processar essas exceções corretamente e mostre informações detalhadas aos usuários nos casos em que os erros podem ser resolvidos. As exceções não processadas interrompem o ambiente de execução do app e podem resultar em falhas.

Como alternativa, use os comandos off() ou on() para definir explicitamente o estado:

onOffTrait.off()
onOffTrait.on()

Depois de enviar um comando para mudar o estado, quando ele for concluído, você poderá ler o estado conforme descrito em Ler o estado de um dispositivo para processá-lo no app. Como alternativa, use fluxos conforme descrito em Observar o estado, que é o método preferido.

Enviar um comando com parâmetros

Alguns comandos podem usar parâmetros, como os das características OnOff ou 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(),
)

Alguns comandos têm argumentos opcionais, que vêm depois dos obrigatórios.

Por exemplo, o comando step para o atributo FanControl tem dois argumentos opcionais:

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 }

Verificar se um atributo é compatível com um trait

Alguns dispositivos podem ser compatíveis com uma característica Matter, mas não com um atributo específico. Por exemplo, um dispositivo Cloud-to-cloud que foi mapeado para Matter pode não ser compatível com todos os atributos Matter. Para lidar com casos como esses, use a função supports no nível do atributo e o tipo enumerado Attribute do atributo para verificar se ele é compatível com um dispositivo específico.

Por exemplo, para verificar se um dispositivo é compatível com o atributo onOff do atributo On/Off:

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

Alguns atributos podem ser nulos na especificação Matter ou no esquema Cloud-to-cloud smart home. Para esses atributos, é possível determinar se um null retornado pelo atributo é devido ao dispositivo não informar esse valor ou se o valor do atributo é realmente null, usando isNullable além 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!")
}

Atualizar atributos de traço

Se você quiser mudar o valor de um determinado atributo, e nenhum dos comandos da característica fizer isso, o atributo poderá aceitar que o valor seja definido explicitamente.

A possibilidade de alterar o valor de um atributo depende de dois fatores:

  • O atributo é gravável?
  • O valor do atributo pode mudar como um efeito colateral do envio de um comando de atributo?

A documentação de referência para traits e os atributos deles fornece essas informações.

Portanto, as combinações de propriedades que determinam como o valor de um atributo pode ser alterado são:

  • Somente leitura e não afetado por outros comandos. Isso significa que o valor do atributo não muda. Por exemplo, o atributo currentPosition do atributo Switch.

  • Somente leitura e afetado por outros comandos. Isso significa que a única maneira de mudar o valor do atributo é enviando um comando. Por exemplo, o atributo currentLevel da característica Matter de LevelControl é somente leitura, mas o valor dele pode ser alterado por comandos como moveToLevel.

  • Gravável e não afetado por outros comandos. Isso significa que é possível mudar diretamente o valor do atributo usando a função update do trait, mas não há comandos que afetem o valor do atributo. Por exemplo, o atributo WrongCodeEntryLimit do atributo DoorLock.

  • Gravável e afetado por outros comandos. Isso significa que é possível mudar diretamente o valor do atributo usando a função update do trait, e o valor do atributo pode mudar como resultado do envio de um comando. Por exemplo, o atributo speedSetting do FanControlTrait pode ser gravado diretamente, mas também é mutável usando o comando step.

Exemplo de uso da função de atualização para mudar o valor de um atributo

Este exemplo mostra como definir explicitamente o valor do atributo DoorLockTrait.WrongCodeEntryLimit.

Para definir o valor de um atributo, chame a função update da trait e transmita uma função mutadora que define o novo valor. É uma boa prática primeiro verificar se o atributo é compatível com um atributo.

Exemplo:

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

Enviar vários comandos de uma só vez

A API Batching permite que um cliente envie vários comandos de dispositivo das APIs Home em um único payload. Os comandos são agrupados em um único payload e executados em paralelo, de forma semelhante à criação de uma automação da API Home usando o nó paralelo, como o exemplo Abrir as persianas antes do nascer do sol. No entanto, a API Batching permite comportamentos mais complexos e sofisticados do que a API Automation, como a capacidade de selecionar dispositivos dinamicamente no momento da execução de acordo com qualquer critério.

Os comandos em um lote podem segmentar vários atributos em vários dispositivos, em vários ambientes e em várias estruturas.

O envio de comandos em lote permite que os dispositivos realizem ações simultaneamente, o que não é possível quando os comandos são enviados sequencialmente em solicitações separadas. O comportamento alcançado com os comandos em lote permite que o desenvolvedor defina o estado de um grupo de dispositivos para corresponder a um estado agregado predeterminado.

Usar a API Batching

Há três etapas básicas envolvidas na invocação de comandos pela API Batching:

  1. Invoque o método Home.sendBatchedCommands().
  2. No corpo do bloco sendBatchedCommands(), especifique os comandos a serem incluídos no lote.
  3. Verifique os resultados dos comandos enviados para saber se eles foram bem-sucedidos ou falharam.

Enviar comandos em lote

Chame o método Home.sendBatchedCommands(). Nos bastidores, esse método configura uma expressão lambda em um contexto de lote especial.

home.sendBatchedCommands() {

Especificar comandos em lote

No corpo do bloco sendBatchedCommands(), preencha os comandos em lote. Os comandos em lote são versões "shadow" dos comandos atuais da API Device que podem ser usados em um contexto de lote e são nomeados com o sufixo Batchable. Por exemplo, o comando moveToLevel() do atributo LevelControl tem uma contraparte chamada moveToLevelBatchable().

Exemplo:

  val response1 = add(command1)

  val response2 = add(command2)

O lote é enviado automaticamente depois que todos os comandos são adicionados ao contexto do lote e a execução sai do contexto.

As respostas são capturadas em objetos DeferredResponse<T>.

As instâncias de DeferredResponse<T> podem ser reunidas em um objeto de qualquer tipo, como um Collection ou uma classe de dados definida por você. O tipo de objeto que você escolher para montar as respostas é o que será retornado por sendBatchedCommands(). Por exemplo, o contexto em lote pode retornar duas instâncias DeferredResponse em um Pair:

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

Como alternativa, o contexto em lote pode retornar as instâncias DeferredResponse em uma classe de dados personalizada:

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

Verificar cada resposta

Fora do bloco sendBatchedCommands(), verifique as respostas para determinar se o comando correspondente foi bem-sucedido ou falhou. Para isso, chame DeferredResponse.getOrThrow(), que: - retorna o resultado do comando executado; - ou, se o escopo do lote não tiver sido concluído ou o comando não tiver sido bem-sucedido, gera um erro.

Verifique os resultados apenas fora do escopo lambda sendBatchedCommands().

Exemplo

Digamos que você queira criar um app que use a API Batching para configurar uma cena de "boa noite" que configure todos os dispositivos da casa para a noite, quando todos estiverem dormindo. Esse app deve apagar as luzes e trancar as portas da frente e de trás.

Veja uma maneira de realizar a tarefa:

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

Controlar um grupo de dispositivos

Além do controle detalhado sobre cada membro de um grupo iterando pelos dispositivos membros, é possível controlar um DeviceGroup como uma única entidade. DeviceGroup implementa a interface PartsProvider, que expõe as partes DeviceType compatíveis com os dispositivos no grupo:

  • group.parts() retorna um Flow<Set<DeviceType>> de todos os tipos de dispositivo presentes no grupo.
  • group.part() e group.partOrNull() retornam um fluxo para um tipo de dispositivo específico, como OnOffLightDevice ou DimmableLightDevice.

Quando você invoca um comando de atributo em um tipo de dispositivo que faz parte de um DeviceGroup, o SDK retransmite o comando para todos os dispositivos membros associados no grupo que oferecem suporte a esse recurso, independentemente da conectividade ou do transporte (como Matter ou Cloud-to-cloud).

Atributos de característica de parte do grupo e tratamento de erros

A segmentação de uma parte do grupo simplifica casos comuns em que você quer enviar um comando idempotente (como acender ou apagar as luzes ou definir um nível de brilho) em um grupo. Ao trabalhar com atributos e comandos em uma parte DeviceGroup, considere os seguintes comportamentos:

  • Os atributos de estado dinâmico (como o atributo onOff da característica OnOff ou o atributo currentLevel da característica LevelControl) geralmente não são preenchidos em uma característica de parte do grupo porque os dispositivos membros em um grupo podem estar em estados diferentes que não podem ser combinados em um único valor. No entanto, os comandos compatíveis (verificados com supports(), como LevelControl.Command.MoveToLevelWithOnOff em uma luz regulável) e alguns atributos de capacidade (como colorTempPhysicalMinMireds e colorTempPhysicalMaxMireds em ColorControl para luzes coloridas) são preenchidos para que o app possa configurar os controles de interface adequadamente.
  • Nem todos os comandos em características de membros do grupo estão disponíveis em uma parte do grupo. Em particular, os comandos que retornam valores não estão disponíveis.
  • Um comando em uma parte do grupo não retorna resultados individuais para cada comando de dispositivo membro subjacente. Para inspecionar resultados de comandos individuais ou chamar comandos que retornam valores, itera nos dispositivos membros do grupo diretamente.
  • Os comandos enviados a uma parte em um DeviceGroup são concluídos quando todo o fan-out nos dispositivos membros é concluído e todos os sucessos ou falhas de comando subjacentes são coletados. Um comando só falha (gera um HomeException) se falhar para todos os dispositivos membros associados ou se todos os dispositivos de destino estiverem indisponíveis, aparecendo como uma única falha de comando que corresponde à sintaxe de chamada de comando único. Se pelo menos um dispositivo membro executar o comando, o comando de grupo será concluído sem gerar uma exceção.

Enviar comandos para uma parte do grupo

Para acender todas as luzes OnOffLightDevice em um grupo de dispositivos:

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

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

Também é possível inspecionar todos os tipos de dispositivo presentes em um grupo usando group.parts() para segmentar um atributo em vários tipos de dispositivos relacionados (por exemplo, desligar qualquer tipo de dispositivo de luz no grupo que seja compatível com 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()

É possível verificar o suporte a comandos ou ler os limites de capacidade em uma parte do grupo antes de enviar um comando (por exemplo, verificar MoveToLevelWithOnOff em uma parte de luz com intensidade regulável ou ler colorTempPhysicalMinMireds e colorTempPhysicalMaxMireds em uma parte de luz com temperatura de cor):

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