Aproveitar a descoberta de dispositivos

A API Discovery foi desenvolvida para ser usada por apps que podem criar automações com base nos dispositivos presentes na casa do usuário. Ele pode revelar a um app em tempo de execução quais características e dispositivos estão presentes em uma determinada estrutura para uso em automações. Além disso, ele expõe os comandos, atributos e eventos associados, bem como o intervalo de valores permitidos para parâmetros e campos.

A API Discovery ignora todos os dispositivos ou atributos que existem em uma estrutura que não é compatível com a API Automation, bem como todos os dispositivos ou atributos que não foram registrados no FactoryRegistry. Consulte Criar uma instância do Google Home para mais informações sobre como usar FactoryRegistry.

Usar a API

O núcleo da API Discovery é a interface HasCandidates, que é a raiz de uma hierarquia de tipos que inclui Structure, Room e HomeDevice.

A interface HasCandidates define dois métodos, candidates() e allCandidates(), que retornam objetos Flow.

  • candidates() produz uma lista de candidatos à automação para a entidade (Structure, Room, HomeDevice).

  • allCandidates() produz uma lista de candidatos à automação para a entidade e todos os filhos dela. Esse método não é compatível com Room.

Ao contrário dos outros objetos Flow retornados pelas APIs do Google Home, eles contêm um instantâneo único.

Para conseguir a lista mais atualizada de candidatos disponíveis, o desenvolvedor precisa chamar candidates() ou allCandidates() toda vez. Não é possível chamar collect() nos objetos Flow. Além disso, como esses dois métodos são especialmente intensivos em recursos, chamá-los mais de uma vez por minuto resulta na devolução de dados armazenados em cache, que podem não refletir o estado atual no momento.

A interface NodeCandidate representa um nó candidato encontrado por esses dois métodos e é a raiz de uma hierarquia que inclui as seguintes interfaces:

e as seguintes classes:

Trabalhar com candidatos à automação

Digamos que você esteja escrevendo um app que cria uma automação para fechar um conjunto de persianas inteligentes em um horário especificado pelo usuário. No entanto, você não sabe se o usuário tem um dispositivo compatível com a característica WindowCovering e se WindowCovering ou qualquer um dos atributos ou comandos dela pode ser usado em automações.

O código a seguir ilustra como usar a API Discovery para filtrar a saída do método candidates() para restringir os resultados e receber o tipo específico de elemento (estrutura, evento, comando) que está sendo procurado. No final, ele cria uma automação com os elementos coletados.

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

O exemplo a seguir cria uma automação para definir o nível de brilho de uma luz quando ela é ligada.

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

Verificar os pré-requisitos

A API Discovery informa que uma característica não tem um pré-requisito para uso, como uma assinatura ou um endereço de estrutura. Isso é feito usando o atributo unsupportedReasons da classe Candidate. Esse atributo é preenchido com um UnsupportedCandidateReason durante a chamada candidates(). E as mesmas informações aparecem nas mensagens de erro de validação quando createAutomation() é chamado.

Exemplos de motivos:

Por exemplo, para processar o MissingStructureAddressSetup UnsupportedCandidateReason, você pode mostrar um aviso no app e abrir o GHA para permitir que o usuário forneça o endereço da estrutura:

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

Validar parâmetros

A API Discovery retorna os valores permitidos para um campo de atributo, parâmetro ou evento, na forma de uma instância de Constraint. Essas informações permitem que o desenvolvedor do app impeça que os usuários definam valores inválidos.

Cada uma das subclasses de Constraint tem uma maneira própria de representar os valores aceitos.

Tabela: tipos de Constraint
Classe de restrição Propriedades que representam valores aceitos
BitmapConstraint combinedBits
BooleanConstraint
ByteConstraint maxLength e minLength
EnumConstraint allowedSet
NumberRangeConstraint lowerBound, upperBound, step e unit
NumberSetConstraint allowedSet e unit
StringConstraint allowedSet, disallowedSet, isCaseSensitive, maxLength, minLength e regex
StructConstraint fieldConstraints
ListConstraint elementConstraint

Usar restrições

Digamos que você esteja escrevendo um app que cria uma automação que define o nível de um dispositivo com o traço LevelControl. O exemplo a seguir mostra como garantir que o valor usado para definir o atributo currentLevel do atributo LevelControl esteja dentro dos limites aceitos.


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
}

Comparar a API Device e a API Discovery

É possível descobrir tipos de dispositivo, características e atributos sem usar a API Discovery. Com a API Device, você pode descobrir:

  1. Os tipos de dispositivo principal que o usuário concedeu permissão ao desenvolvedor para controlar, usando o método DeviceType.Metadata.isPrimaryType().
  2. Se cada dispositivo oferece suporte a todas as características necessárias para a automação, usando o método HasTraits.has().
  3. Se cada trait oferece suporte a todos os atributos e comandos que a automação requer, usando o método supports().

É importante observar que, se você usar a API Device para fazer descobertas, não vai se beneficiar dos seguintes recursos da API Discovery:

  • Filtragem automática de traços que não são compatíveis com a API Automation.
  • A capacidade de oferecer aos usuários uma opção para selecionar um valor válido para esses atributos e parâmetros que usam restrições.