Leverage device discovery

The Discovery API is intended to be used by apps that can create automations based on the devices present in the user's home. It can reveal to an app at runtime what traits and devices are present in a given structure for use in automations. In addition, it exposes the associated commands, attributes, and events, as well as the range of values that are allowed for parameters and fields.

The Discovery API ignores any devices or traits that exist in a structure that aren't supported by the Automation API, as well as any devices or traits that weren't registered in the FactoryRegistry. See Create a Home instance for more information on using FactoryRegistry.

Use the API

At the core of the Discovery API is the HasCandidates interface, which is the root of a type hierarchy that includes Structure, Room, and HomeDevice.

The HasCandidates interface defines two methods, candidates() and allCandidates() that both return Flow objects.

  • candidates() produces a list of automation candidates for the entity (Structure, Room, HomeDevice).

  • allCandidates() produces a list of automation candidates for the entity and all its children. This method is not supported by Room.

Unlike the other Flow objects returned by the Home APIs, these contain a one-time snapshot.

To obtain the most up-to-date list of available candidates, the developer must call candidates() or allCandidates() each time, and cannot just call collect() on the Flow objects. Furthermore, because these two methods are especially resource-intensive, calling them more often than once per minute will result in cached data being returned, which may not reflect the actual current state at that moment.

The NodeCandidate interface represents a candidate node found by these two methods, and is the root of a hierarchy which includes the following interfaces:

and the following classes:

Work with automation candidates

Say you're writing an app that creates an automation to close a set of smart blinds at a user-specified time. However, you don't know whether the user has a device that supports the WindowCovering trait and whether WindowCovering or any of its attributes or commands can be used in automations.

The following code illustrates how to use the Discovery API to filter the output of the candidates() method to narrow down the results and obtain the specific kind of element (structure, event, command) being sought. At the end, it creates an automation out of the collected elements.

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

The following example creates an automation to set the brightness level of a light when it is turned on.

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

Check for prerequisites

The Discovery API lets you know a trait is missing a prerequisite for use, such as a subscription or a structure address. It does this using the Candidate class's unsupportedReasons attribute. This attribute is populated with an UnsupportedCandidateReason during the candidates() call. And the same information appears in the validation error messages when createAutomation() is called.

Example reasons:

For example, to handle the MissingStructureAddressSetup UnsupportedCandidateReason, you might want to show a toast in your app and open the GHA to allow the user to provide the address of the structure:

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

Validate parameters

The Discovery API returns the values that are allowed for an attribute, parameter, or event field, in the form of a Constraint instance. This information allows the app developer to prevent users from setting invalid values.

Each of the subclasses of Constraint have their own way to represent accepted values.

Table: Types of Constraint
Constraint class Properties representing accepted values
BitmapConstraint combinedBits
BooleanConstraint
ByteConstraint maxLength and minLength
EnumConstraint allowedSet
NumberRangeConstraint lowerBound, upperBound, step and unit
NumberSetConstraint allowedSet and unit
StringConstraint allowedSet, disallowedSet, isCaseSensitive, maxLength, minLength, and regex
StructConstraint fieldConstraints
ListConstraint elementConstraint

Use constraints

Say you're writing an app that creates an automation that sets the level of a device with the LevelControl trait. The following example shows how you would ensure that the value used to set the LevelControl trait's currentLevel attribute is within the accepted bounds.


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
}

Compare the Device API and Discovery API

You can discover device types, traits, and their attributes without using the Discovery API. Using the Device API, you can discover:

  1. The primary device types the user has granted permission to the developer for control, using the DeviceType.Metadata.isPrimaryType() method.
  2. Whether each device supports all the traits the automation requires, using the HasTraits.has() method.
  3. Whether each trait supports all attributes and commands the automation requires, using the supports() method.

It's important to note that if you use the Device API to do discovery, you don't benefit from the following Discovery API capabilities:

  • Automatic filtering out of traits that aren't supported by the Automation API.
  • The ability to provide users with an option to select a valid value for those attributes and parameters that use constraints.