Wykorzystanie wykrywania urządzeń

Interfejs Discovery API jest przeznaczony do tworzenia automatyzacji na podstawie urządzeń znajdujących się w domu użytkownika. Może ona ujawnić aplikacji w czasie działania, jakie cechy i urządzenia są dostępne w danej strukturze na potrzeby automatyzacji. Dodatkowo zawiera powiązane polecenia, atrybuty i zdarzenia, a także zakres wartości dozwolonych dla parametrów i pol.

Interfejs Discovery API ignoruje urządzenia i cechy, które istnieją w strukturze, ale nie są obsługiwane przez interfejs Automation API, a także urządzenia i cechy, które nie zostały zarejestrowane w FactoryRegistry. Więcej informacji o używaniu FactoryRegistry znajdziesz w artykule Tworzenie instancji Home.

Korzystanie z interfejsu API

Interfejs HasCandidates jest podstawą interfejsu Discovery API. Stanowi on korzeń hierarchii typów, która obejmuje typy Structure, RoomHomeDevice.

Interfejs HasCandidates definiuje 2 metody: candidates()allCandidates(), które zwracają obiekty Flow.

  • candidates() zwraca listę kandydatów do automatyzacji dla danego elementu (Structure, Room, HomeDevice).

  • allCandidates() generuje listę kandydatów do automatyzacji dla danego elementu i wszystkich jego elementów podrzędnych. Ta metoda nie jest obsługiwana przez Room.

W odróżnieniu od innych obiektów Flow zwracanych przez interfejsy API Home te obiekty zawierają jednorazowy zrzut ekranu.

Aby uzyskać aktualną listę dostępnych kandydatów, deweloper musi za każdym razem wywołać funkcję candidates() lub allCandidates(), a nie może po prostu wywołać funkcji collect() na obiektach Flow. Ponadto te 2 metody są szczególnie obciążające pod względem zasobów, więc ich wywoływanie częściej niż raz na minutę spowoduje zwrócenie danych z pamięci podręcznej, które mogą nie odzwierciedlać rzeczywistego stanu w danym momencie.

Interfejs NodeCandidate reprezentuje węzeł kandydata znaleziony przez te 2 metody i jest węzłem korzenia hierarchii, która obejmuje te interfejsy:

oraz te klasy:

Praca z kandydatami do automatyzacji

Załóżmy, że piszesz aplikację, która tworzy automatyzację zamykania zestawu inteligentnych rolet w określonym przez użytkownika czasie. Nie wiesz jednak, czy użytkownik ma urządzenie obsługujące cechę WindowCovering i czy WindowCovering lub którykolwiek z jego atrybutów lub poleceń może być używany w automatyzacjach.

Poniższy kod pokazuje, jak za pomocą interfejsu Discovery API odfiltrować dane wyjściowe metody candidates(), aby zawęzić wyniki i uzyskać poszukiwany element (strukturę, zdarzenie lub polecenie). Na koniec tworzy automatyzację na podstawie zebranych elementów.

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

W tym przykładzie tworzymy automatyzację, która ustawia jasność światła po włączeniu.

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

Sprawdzanie wymagań wstępnych

Interfejs Discovery API informuje, że cecha nie spełnia wymagań wstępnych, takich jak subskrypcja czy adres strukturalny. Wykorzystuje do tego atrybut Candidate klasy unsupportedReasons. Ten atrybut jest wypełniany wartością UnsupportedCandidateReason podczas wywołania funkcji candidates(). Te same informacje pojawiają się w komunikatach o błędach walidacji, gdy wywoływana jest funkcja createAutomation().

Przykładowe przyczyny:

Na przykład, aby obsłużyć zdarzenie MissingStructureAddressSetup UnsupportedCandidateReason, możesz wyświetlić w aplikacji toast i otworzyć GHA, aby umożliwić użytkownikowi podanie adresu budynku:

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

Weryfikowanie parametrów

Discovery API zwraca wartości dozwolone dla atrybutu, parametru lub pola zdarzenia w postaci instancji Constraint. Dzięki tym informacjom deweloper aplikacji może uniemożliwić użytkownikom ustawianie nieprawidłowych wartości.

Każda z podklas Constraint ma własny sposób przedstawiania akceptowanych wartości.

Tabela: typy Constraint
Klasa ograniczenia Właściwości reprezentujące akceptowane wartości
BitmapConstraint combinedBits
BooleanConstraint
ByteConstraint maxLengthminLength
EnumConstraint allowedSet
NumberRangeConstraint lowerBound, upperBound, stepunit
NumberSetConstraint allowedSetunit
StringConstraint allowedSet, disallowedSet, isCaseSensitive,maxLength, minLength i regex
StructConstraint fieldConstraints
ListConstraint elementConstraint

Używanie ograniczeń

Załóżmy, że piszesz aplikację, która tworzy automatyzację ustawiającą poziom urządzenia z LevelControlw trybie poziomym. Poniższy przykład pokazuje, jak sprawdzić, czy wartość użyta do ustawienia atrybutu currentLevel cechy LevelControl mieści się w akceptowanych granicach.


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
}

Porównanie interfejsów Device API i Discovery API

Typy urządzeń, ich cechy i atrybuty możesz wykrywać bez korzystania z interfejsu Discovery API. Za pomocą interfejsu Device API możesz uzyskać informacje o tych elementach:

  1. Główne typy urządzeń, na których użytkownik przyznał deweloperowi uprawnienia do sterowania, za pomocą metody DeviceType.Metadata.isPrimaryType().
  2. Czy każde urządzenie obsługuje wszystkie cechy wymagane przez automatyzację, za pomocą metody HasTraits.has().
  3. Określ, czy każda cecha obsługuje wszystkie atrybuty i polecenia wymagane przez automatyzację, korzystając z metody supports().

Pamiętaj, że jeśli do wykrywania używasz interfejsu Device API, nie możesz korzystać z tych funkcji interfejsu Discovery API:

  • Automatyczne odfiltrowywanie cech, które nie są obsługiwane przez interfejs Automation API.
  • Możliwość udostępnienia użytkownikom opcji wyboru prawidłowej wartości atrybutów i parametrów, które korzystają z ograniczeń.