デバイス検出を活用する

Discovery API は、ユーザーの自宅にあるデバイスに基づいて自動化を作成できるアプリで使用することを想定しています。自動化で使用するために、特定の構造に存在する特徴とデバイスを実行時にアプリに公開できます。また、関連するコマンド、属性、イベント、パラメータとフィールドに許可される値の範囲も公開します。

Discovery API は、Automation API でサポートされていない構造に存在するデバイスや特性、FactoryRegistry に登録されていないデバイスや特性を無視します。FactoryRegistry の使用方法については、Home インスタンスを作成するをご覧ください。

API を使用する

Discovery API の中核となるのは HasCandidates インターフェースです。これは、StructureRoomHomeDevice を含む型階層のルートです。

HasCandidates インターフェースには、candidates()allCandidates() の 2 つのメソッドが定義されています。どちらも Flow オブジェクトを返します。

  • candidates() は、エンティティの自動化候補のリスト(StructureRoomHomeDevice)を生成します。

  • allCandidates() は、エンティティとそのすべての子に対する自動化候補のリストを生成します。このメソッドは Room ではサポートされていません。

Home API から返される他の Flow オブジェクトとは異なり、これらのオブジェクトには 1 回限りのスナップショットが含まれています。

使用可能な候補の最新リストを取得するには、デベロッパーは毎回 candidates() または allCandidates() を呼び出す必要があります。Flow オブジェクトで collect() を呼び出すだけでは不十分です。さらに、これらの 2 つのメソッドは特にリソースを消費するため、1 分間に 1 回を超える頻度で呼び出すと、キャッシュに保存されたデータが返され、その時点での実際の状態が反映されない可能性があります。

NodeCandidate インターフェースは、これらの 2 つのメソッドで検出された候補ノードを表します。また、次のインターフェースを含む階層のルートです。

および次のクラス:

自動化の候補を操作する

ユーザーが指定した時刻にスマートブラインドを閉じる自動化を作成するアプリを作成しているとします。ただし、ユーザーが 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 は、Time トレイトを使用するには住所の設定が必要であることをユーザーに知らせます。Google Home の住所を変更するでは、ユーザーが Google Home app (GHA) を使用して建物の住所を入力する方法について説明します。
  • MissingPresenceSensingSetup は、AreaPresenceStateAreaAttendanceState の特徴を使用するには、在席ステータスの設定が必要であることをユーザーに知らせます。
  • MissingSubscription は、ObjectDetection トレイトを使用するには Google Nest Aware の定期購入が必要であることをユーザーに知らせます。

たとえば、MissingStructureAddressSetup UnsupportedCandidateReason を処理するには、アプリにトーストを表示し、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 でサポートされていない特徴の自動除外。
  • 制約を使用する属性とパラメータに有効な値を選択するオプションをユーザーに提供する機能。