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](https://developers.home.google.com/apis/ios/initialize.md.txt#home_configuration) for more
information.

> [!CAUTION]
> **Caution:** Not all devices report state changes as they should. Devices that don't reliably report state changes shouldn't be used as starters. The Discovery API filters out devices that have been identified as having this issue. However, not all such devices may have been identified, therefore the Discovery API can't be viewed as infallible in this regard.

> [!IMPORTANT]
> **Important:** Discovery API calls can be resource-intensive and incur significant latency. Developers should be careful not to allow this latency to impact user interface responsiveness.

## Use the API

At the core of the Discovery API for iOS are two structure-level method:

- [`candidates(includeDescendants:)`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/Structure.md.txt#candidatesincludedescendants:) --- Returns a list of automation candidate nodes for a structure. Set `includeDescendants to true` to get child candidates for all members of the structure.
- [`candidates(for:)`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/Structure.md.txt#candidatesfor:) --- 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`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/NodeCandidate.md.txt)
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:

- [`ActionCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/ActionCandidate.md.txt)
- [`StarterCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/StarterCandidate.md.txt)
- [`StateReaderCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/StateReaderCandidate.md.txt)

The following candidates are structs that correspond to the elements that can
be used in starters, actions, and state readers:

- [`CommandCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/CommandCandidate.md.txt)
- [`EventCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/EventCandidate.md.txt)
- [`TraitAttributesCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/TraitAttributesCandidate.md.txt)

## 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.

```swift
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.

```swift
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`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/TraitAttributesCandidate.md.txt)
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`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/TraitAttributesCandidate.md.txt#fielddetails),
which is a map of
[`AttributeFieldDetails`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/AttributeFieldDetails.md.txt)
instances, each one representing one of the trait's attributes.

So, to determine what an attribute supports:

- Get a handle to the [`TraitAttributesCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/TraitAttributesCandidate.md.txt) that represents a trait that you want to use in your automation.
- Retrieve the trait's attributes through the [`TraitAttributesCandidate.fieldDetails`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/TraitAttributesCandidate.md.txt#fielddetails).
- Check the [`AttributeFieldDetails`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/AttributeFieldDetails.md.txt) instance for the attribute you're interested in:
  - To determine whether the attribute can be used as a starter, check [`AttributeFieldDetails.isSubscribable`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/AttributeFieldDetails.md.txt#issubscribable).
  - To determine whether an attribute can be updated, check [`AttributeFieldDetails.isModifiable`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/AttributeFieldDetails.md.txt#ismodifiable).

## 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`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/NodeCandidate.md.txt)
protocol, which all candidates protocols and structures
([`CommandCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/CommandCandidate.md.txt)),
([`EventCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/EventCandidate.md.txt), and
[`TraitAttributesCandidate`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/TraitAttributesCandidate.md.txt))
conform to and is populated with an
[`UnsupportedCandidateReason`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/UnsupportedCandidateReason.md.txt)
list of
[`UnsupportedCandidateReasonTypes`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/UnsupportedCandidateReasonType.md.txt)
during either `candidates()` call. The same information also appears in the
validation error messages when
[`createAutomation(_:)`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/AutomationManager.md.txt?db=home-features#createautomation_:)
is called.

Example reasons:

- [`MissingStructureAddressSetup`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/UnsupportedCandidateReasonType.md.txt#missingstructureaddresssetup) lets the user know that address setup is required in order to use the `Time` trait. If a structure was created without an address, [Change Google home address](https://support.google.com/googlenest/answer/7551002) explains how a user can enter the structure address using the Google Home app (GHA).
- [`MissingPresenceSensingSetup`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/UnsupportedCandidateReasonType.md.txt#missingpresencesensingsetupsetuptype:) lets the user know that presence setup is required in order to use `AreaPresenceState` and `AreaAttendanceState` traits. Setup types are defined in [`PresenceSensingSetupType`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/PresenceSensingSetupType.md.txt).
- [`MissingSubscription`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/UnsupportedCandidateReasonType.md.txt#missingsubscription) lets the user know that a [Google Home Premium](https://store.google.com/product/google_home_premium) subscription is required in order to use the `ObjectDetection` trait.

For example, to handle the `MissingStructureAddressSetup`
`UnsupportedCandidateReason` when an existing structure lacks an address, you
might want to show an alert in your app and direct the user to the
GHA to provide the address of the structure:

```swift
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`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt)
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](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#bitmapconstraintcombinedbits:typeid:) | `combinedBits: [CombinedBitsDescriptor], typeId: String` |
| [BooleanConstraint](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#booleanconstraint) | `booleanConstraint` |
| [ByteConstraint](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#byteconstraintminlength:maxlength:) | `minLength: UInt32, maxLength: UInt32` |
| [EnumConstraint](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#enumconstraintallowedset:) | `allowedSet: Set` |
| [StringConstraint](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#stringconstraintminlength:maxlength:disallowedvalues:disallowedvaluescasesensitive:regex:) | `minLength: UInt32 = 0, maxLength: UInt32 = 256, disallowedValues: [String], disallowedValuesCaseSensitive: Bool = false, regex: String = ""` |
| [StructConstraint](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#structconstraintstructfieldconstraint:) | `structFieldConstraint: [AnyField : Constraint]` |
| [ListConstraint](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/Constraint.md.txt#listconstraintelementconstraint:) | `elementConstraint: Constraint` |
[*Table: Types of `Constraint`*]

### Use constraints

Say you're writing an app that creates an automation that sets the level of a
device with the
[`LevelControlTrait`](https://developers.home.google.com/reference/swift/GoogleHomeTypes/Enums/Matter/LevelControlTrait.md.txt).
The following example shows how you would ensure that the value used to
set the `LevelControlTrait`'s
[`currentLevel`](https://developers.home.google.com/reference/swift/GoogleHomeTypes/Enums/Matter/LevelControlTrait/Attributes.md.txt#currentlevel)
attribute is within the accepted bounds.

```swift
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:

1. The primary device types the user has granted permission to the developer for control, using the [`DeviceType.Metadata.isPrimaryType()`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/DeviceTypeMetadata.md.txt#isprimarytype) attribute.
2. Whether each device supports all the traits the automation requires, using the [`DeviceType.traits.contains(_:)`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Classes/TraitCollection.md.txt#/s:13GoogleHomeSDK15TraitCollectionC8containsySbxmAA0D0RzlF) method.
3. Whether each trait supports all attributes, events, and commands the automation requires, using the `supportedEventTypes` and `supportedCommandTypes` 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.