善用裝置探索

Discovery API 適用於可根據使用者家中裝置建立自動化動作的應用程式。在執行階段向應用程式揭露特定結構體中可用於自動化動作的特徵和裝置。此外,它也會公開相關聯的指令、屬性和事件,以及參數和欄位允許的值範圍。

Discovery API 會忽略結構中存在的任何裝置或特徵,因為 Automation API 不支援這些項目,也會忽略未在 FactoryRegistry 中註冊的任何裝置或特徵。如要進一步瞭解如何使用 FactoryRegistry,請參閱「建立 Home 例項」。

使用 API

Discovery API 的核心是 HasCandidates 介面,這是類型階層的根目錄,其中包含 StructureRoomHomeDevice

HasCandidates 介面定義了兩個方法:candidates()allCandidates(),兩者都會傳回 Flow 物件。

  • candidates() 會產生實體 (StructureRoomHomeDevice) 的自動化候選項目清單。

  • allCandidates() 會為實體及其所有子項產生自動化候選項目清單。Room 不支援這個方法。

與 Home API 傳回的其他 Flow 物件不同,這些物件包含一次性的快照。

為了取得最新的可用候選清單,開發人員必須每次都呼叫 candidates()allCandidates(),而不能只在 Flow 物件上呼叫 collect()。此外,由於這兩種方法特別耗用資源,因此如果每分鐘呼叫次數超過一次,系統就會傳回快取資料,而這可能無法反映當下的實際狀態。

NodeCandidate 介面代表這兩種方法找到的候選節點,也是包含下列介面的階層的根:

以及下列類別:

與自動化候選項目合作

假設您正在編寫應用程式,以便建立自動化動作,在使用者指定的時間關閉一組智慧百葉窗。不過,您無法得知使用者是否擁有支援 WindowCovering 特徵的裝置,也無法得知 WindowCovering 或其任何屬性或指令是否可用於自動化動作。

以下程式碼說明如何使用 Discovery API 篩選 candidates() 方法的輸出內容,以便縮小結果範圍,並取得所尋找的特定元素類型 (結構、事件、指令)。最後,系統會根據收集到的元素建立自動化動作。

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

以下範例會建立自動化動作,在燈具開啟時設定亮度等級。

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

檢查是否符合先決條件

Discovery API 會告知您特徵缺少使用前置條件,例如訂閱或結構體位址。這會使用 Candidate 類別的 unsupportedReasons 屬性。在 candidates() 呼叫期間,系統會使用 UnsupportedCandidateReason 填入這個屬性。呼叫 createAutomation() 時,驗證錯誤訊息也會顯示相同的資訊。

原因示例:

舉例來說,如要處理 MissingStructureAddressSetup UnsupportedCandidateReason,您可能會想在應用程式中顯示Toast,並開啟 GHA,讓使用者提供結構體的地址:

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

驗證參數

Discovery API 會以 Constraint 例項的形式,傳回屬性、參數或事件欄位允許的值。這項資訊可讓應用程式開發人員避免使用者設定無效值。

Constraint 的每個子類別都有各自的方式來表示可接受的值。

表格:Constraint 的類型
限制類別 代表接受值的屬性
BitmapConstraint combinedBits
BooleanConstraint
ByteConstraint maxLengthminLength
EnumConstraint allowedSet
NumberRangeConstraint lowerBoundupperBoundstepunit
NumberSetConstraint allowedSetunit
StringConstraint allowedSetdisallowedSetisCaseSensitivemaxLengthminLengthregex
StructConstraint fieldConstraints
ListConstraint elementConstraint

使用限制

假設您編寫的應用程式會建立自動化動作,並使用 LevelControl 特徵設定裝置的音量。以下範例說明如何確保用於設定 LevelControl 特徵的 currentLevel 屬性值,符合可接受的範圍。


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
}

比較 Device API 和 Discovery API

您可以不使用 Discovery API 來探索裝置類型、特徵及其屬性。使用 Device API 時,您可以探索以下項目:

  1. 使用者已使用 DeviceType.Metadata.isPrimaryType() 方法授予開發人員控制權的主要裝置類型。
  2. 使用 HasTraits.has() 方法,判斷每部裝置是否支援自動化動作所需的所有特徵。
  3. 是否使用 supports() 方法,判斷每個特徵是否支援自動化動作所需的所有屬性指令

請注意,如果您使用 Device API 進行探索,就無法享有下列 Discovery API 功能的優勢:

  • 自動篩除 Automation API 不支援的特徵。
  • 為使用者提供選項,讓他們可以為使用限制的屬性和參數選取有效值。