The Discovery API for iOS 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 during Home configuration. See Home configuration for more information.
Use the API
At the core of the Discovery API for iOS are two structure-level method:
candidates(includeDescendants:)
— Returns a list of automation candidate nodes for a structure. SetincludeDescendates to true
to get child candidates for all members of the structure.candidates(for:)
— Returns a list of automation candidate nodes for a device in a structure.
Since the initial release of the Discovery API is non-streaming, these methods return a one-time snapshot.
To obtain the most up-to-date list of available candidates, the developer must
call candidates(includeDescendants:)
or candidates(for:)
each time.
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
protocol represents a candidate node and defines the common attributes for all
Discovery candidates.
Each of the following protocols are used to convert a candidate to the appropriate node for use in an automation graph:
The following candidates are structs that correspond to the elements that can be used in starters, actions, and state readers:
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 WindowCoveringTrait
trait and whether
WindowCoveringDeviceType
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 GoogleHomeSDK
import GoogleHomeTypes
private func createAutomationWithDiscoveryAPITimeStarter(
structureName: String,
scheduledTime: TimeOfDay
) async throws -> String {
guard
let structure = try await self.home.structures().list().first(where: {
$0.name == structureName
})
else {
return ""
}
guard
try await structure.candidates().list()
.compactMap({ $0.node as? EventCandidate })
.contains(where: {
$0.event is GoogleHomeTypes.Google.TimeTrait.ScheduledEvent.Type
&& $0.unsupportedReasons.isEmpty
})
else {
return ""
}
guard
try await structure.candidates().list()
.compactMap({ $0.node as? CommandCandidate })
.contains(where: {
$0.command is Matter.WindowCoveringTrait.DownOrCloseCommand.Type
&& $0.unsupportedReasons.isEmpty
})
else {
return ""
}
var blindsDevice: HomeDevice? = nil
// prompt user to select the WindowCoveringDevice
...
guard let blindsDevice else {
return ""
}
// Create the draft automation.
let draftAutomation = automation(
name: "",
description: ""
) {
starter(structure, Google.TimeTrait.ScheduledEvent.self) {
GoogleHomeTypes.Google.TimeTrait.ScheduledEvent.clockTime(scheduledTime)
}
action(blindsDevice, WindowCoveringDeviceType.self) {
Matter.WindowCoveringTrait.downOrClose()
}
}
// Create the automation in the structure.
let automation = try await structure.createAutomation(draftAutomation)
return automation.id
}
The following example creates an automation to set the brightness level of a light when it is turned on.
import GoogleHomeSDK
import GoogleHomeTypes
private func createAutomationWithDiscoveryAPIForDimmableLight(structureName: String) async throws -> String {
// Get the structure.
let structures = try await self.home.structures().list()
guard let structure = structures.first(where: { $0.name == structureName }) else {
return ""
}
// Get the devices in the structure.
let structureDevices = try await self.home.devices().list().filter {
$0.structureID == structure.id
}
// Get the candidates.
let allCandidates = try await structure.candidates().list()
// Get the first dimmable light device.
guard
let dimmableLightDevice = structureDevices.first(where: {
$0.types.contains(DimmableLightDeviceType.self)
})
else {
return ""
}
// Validate that the `Matter.OnOffTrait` attribute starter candidate is present for the dimmable
// light device.
guard
allCandidates
.compactMap({ $0.node as? TraitAttributesCandidate })
.contains(where: {
$0.homeObject.id == dimmableLightDevice.id && $0.trait is Matter.OnOffTrait.Type
})
else {
return ""
}
// Validate that the `LevelControlTrait.MoveToLevelCommand`` candidate is present for the
// dimmable light device.
guard
allCandidates
.compactMap({ $0.node as? CommandCandidate })
.contains(where: {
$0.homeObject.id == dimmableLightDevice.id
&& $0.command is Matter.LevelControlTrait.MoveToLevelCommand.Type
})
else {
return ""
}
// Create the draft automation.
let draftAutomation = automation {
let onOffStarter = starter(
dimmableLightDevice, OnOffLightDeviceType.self, Matter.OnOffTrait.self)
condition { onOffStarter.onOff.equals(true) }
action(dimmableLightDevice, DimmableLightDeviceType.self) {
Matter.LevelControlTrait.moveToLevel(
level: 55,
transitionTime: nil,
optionsMask: Matter.LevelControlTrait.OptionsBitmap(),
optionsOverride: Matter.LevelControlTrait.OptionsBitmap()
)
}
}
// Create the automation in the structure.
let automation = try await structure.createAutomation(draftAutomation)
return automation.id
}
Verify that a trait attribute supports your use case
While all trait attributes can be used as a state reader, not all trait attributes can be used as a starter, and not all trait attributes can be modified. You should check each attribute that you intend to use to see whether it supports what you want to do with it.
The
TraitAttributesCandidate
is the starting point for checking attributes. This class represents a trait.
Each trait has one or more attributes associated with it. The trait's
attributes are found in the
TraitAttributesCandidate.fieldDetails
,
which is a map of
AttributeFieldDetails
instances, each one representing one of the trait's attributes.
So, to determine what an attribute supports:
- Get a handle to the
TraitAttributesCandidate
that represents a trait that you want to use in your automation. - Retrieve the trait's attributes through the
TraitAttributesCandidate.fieldDetails
. - Check the
AttributeFieldDetails
instance for the attribute you're interested in:- To determine whether the attribute can be used as a starter, check
AttributeFieldDetails.isSubscribable
. - To determine whether an attribute can be updated, check
AttributeFieldDetails.isModifiable
.
- To determine whether the attribute can be used as a starter, check
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 NodeCandidate
's unsupportedReasons
attribute.
This attribute is provided by the
NodeCandidate
protocol, which all candidates protocols and structures
(CommandCandidate
),
(EventCandidate
, and
TraitAttributesCandidate
)
conform to and is populated with an
UnsupportedCandidateReason
list of
UnsupportedCandidateReasonTypes
during either candidates()
call. The same information also 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. Setup types are defined inPresenceSensingSetupType
.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 an alert in your app and
open the GHA to allow the user to provide the address of
the structure:
guard let structure = try await home.structures().list().first else { return }
let allCandidates = try await structure.candidates().list()
guard
let scheduledStarterCandidate =
allCandidates
.compactMap({ $0.node as? EventCandidate })
.first(where: {
$0.event is GoogleHomeTypes.Google.TimeTrait.ScheduledEvent.Type
})
else {
return
}
if scheduledStarterCandidate.unsupportedReasons.contains(where: {
$0.reasonType == .missingStructureAddressSetup
}) {
showToast("No Structure Address setup. Redirecting to GHA to setup 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: [CombinedBitsDescriptor], typeId: String
|
BooleanConstraint |
booleanConstraint
|
ByteConstraint |
minLength: UInt32, maxLength: UInt32
|
EnumConstraint |
allowedSet: Set
|
StringConstraint |
minLength: UInt32 = 0, maxLength: UInt32 = 256, disallowedValues: [String], disallowedValuesCaseSensitive: Bool = false, regex: String = ""
|
StructConstraint |
structFieldConstraint: [AnyField : Constraint]
|
ListConstraint |
elementConstraint: Constraint
|
Use constraints
Say you're writing an app that creates an automation that sets the level of a
device with the
LevelControlTrait
.
The following example shows how you would ensure that the value used to
set the LevelControlTrait
's
currentLevel
attribute is within the accepted bounds.
import GoogleHomeSDK
import GoogleHomeTypes
...
guard
let levelCommandCandidate = try await structure.candidates().list()
.compactMap({ $0.node as? CommandCandidate })
.first(where: {
$0.command is Matter.LevelControlTrait.MoveToLevelCommand.Type
})
else {
return
}
let field = AnyField(
erasing: Matter.LevelControlTrait.MoveToLevelCommand.CommandRequestFields.level)
guard let levelConstraint = levelCommandCandidate.fieldDetails[field]?.constraint else {
return
}
guard case .uintRangeConstraint(let lowerBound, let upperBound, _, _) = levelConstraint else {
return
}
if (lowerBound...upperBound).contains(value) {
// 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()
attribute. - Whether each device supports all the traits the automation requires, using
the
DeviceType.traits.contains(_:)
method. - Whether each trait supports all attributes, events, and commands the
automation requires, using the
supportedEventTypes
andsupportedCommandTypes
methods specific to each trait.
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.