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 byRoom
.
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:
MissingStructureAddressSetup
lets the user know that address setup is required in order to use theTime
trait. Change Google home address explains how a user can enter the structure address using the Google Home app (GHA).MissingPresenceSensingSetup
lets the user know that presence setup is required in order to useAreaPresenceState
andAreaAttendanceState
traits.MissingSubscription
lets the user know that a Nest Aware subscription is required in order to use theObjectDetection
trait.
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.
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:
- The primary device types the user has granted permission to the developer for
control, using the
DeviceType.Metadata.isPrimaryType()
method. - Whether each device supports all the traits the automation requires, using
the
HasTraits.has()
method. - 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.