Android でデバイスを操作する

トレイトがコマンドをサポートしているかどうかを確認する

トレイト コマンドでもサポートを確認できます。また、トレイトレベルの supports 関数を使用して、特定のデバイスでコマンドがサポートされているかどうかを確認します。

たとえば、On/Off トレイトの toggle コマンドをデバイスがサポートしているかどうかを確認するには、次のようにします。

// Check if the OnOff trait supports the toggle command.
if (onOffTrait.supports(OnOff.Command.Toggle)) {
  println("onOffTrait supports toggle command")
} else {
  println("onOffTrait does not support stateful toggle command")
}

デバイスにコマンドを送信する

コマンドの送信は、トレイトから状態属性を読み取ることに似ています。デバイスのオン / オフを切り替えるには、OnOff トレイトの切り替えコマンドを使用します。このコマンドは、Google Home エコシステム データモデルで toggle() として定義されています。このメソッドは、onOfftrue の場合は false に、false の場合は true に変更します。

// Calling a command on a trait.
try {
  onOffTrait.toggle()
} catch (e: HomeException) {
  // Code for handling the exception
}

すべての特性コマンドは suspend 関数であり、API からレスポンスが返された場合にのみ完了します(デバイスの状態が変更されたことを確認するなど)。実行フローで問題が検出されると、コマンドが例外を返すことがあります。デベロッパーは、try-catch ブロックを使用してこれらの例外を適切に処理し、エラーに対処できる場合はユーザーに詳細情報を表示する必要があります。例外が処理されないと、アプリのランタイムが停止し、アプリがクラッシュする可能性があります。

または、off() コマンドまたは on() コマンドを使用して状態を明示的に設定します。

onOffTrait.off()
onOffTrait.on()

状態を変更するコマンドを送信したら、コマンドが完了したら、デバイスの状態を読み取るで説明されているように状態を読み取り、アプリで処理できます。または、状態をモニタリングするで説明されているようにフローを使用します。これは推奨される方法です。

パラメータ付きのコマンドを送信する

一部のコマンドは、OnOff または LevelControl トレイトのパラメータを使用します。

offWithEffectmoveToLevel
// Turn off the light using the DyingLight effect.
onOffTrait.offWithEffect(
  effectIdentifier = OnOffTrait.EffectIdentifierEnum.DyingLight,
  effectVariant = 0u,
)
// Change the brightness of the light to 50%
levelControlTrait.moveToLevel(
  level = 127u.toUByte(),
  transitionTime = null,
  optionsMask = LevelControlTrait.OptionsBitmap(),
  optionsOverride = LevelControlTrait.OptionsBitmap(),
)

一部のコマンドには、必須の引数の後に省略可能な引数があります。

たとえば、FanControl トレイトの step コマンドには、2 つのオプション引数があります。

val fanControlTraitFlow: Flow<FanControl?> =
  device.type(FanDevice).map { it.standardTraits.fanControl }.distinctUntilChanged()

val fanControl = fanControlTraitFlow.firstOrNull()

// Calling a command with optional parameters not set.
fanControl?.step(direction = FanControlTrait.StepDirectionEnum.Increase)

// Calling a command with optional parameters.
fanControl?.step(direction = FanControlTrait.StepDirectionEnum.Increase) { wrap = true }

トレイトが属性をサポートしているかどうかを確認する

デバイスによっては、Matter トレイトはサポートしていても、特定の属性はサポートしていない場合があります。たとえば、Matter にマッピングされた Cloud-to-cloud デバイスは、すべての Matter 属性をサポートしていない場合があります。このようなケースを処理するには、トレイトレベルの supports 関数とトレイトの Attribute 列挙型を使用して、特定のデバイスで属性がサポートされているかどうかを確認します。

たとえば、On/Off トレイトの onOff 属性をデバイスがサポートしているかどうかを確認するには、次のようにします。

// Check if the OnOff trait supports the onOff attribute.
if (onOffTrait.supports(OnOff.Attribute.onOff)) {
  println("onOffTrait supports onOff state")
} else {
  println("onOffTrait is for a command only device!")
}

一部の属性は、Matter 仕様または Cloud-to-cloud smart home スキーマで null にできます。これらの属性の場合、supports に加えて isNullable を使用すると、属性から返された null がデバイスがその値を報告していないためか、属性の値が実際に null であるかを判断できます。

// Check if a nullable attribute is set or is not supported.
if (onOffTrait.supports(OnOff.Attribute.startUpOnOff)) {
  // The device supports startupOnOff, it is safe to expect this value in the trait.
  if (OnOff.Attribute.startUpOnOff.isNullable && onOffTrait.startUpOnOff == null) {
    // This value is nullable and set to null. Check the specification as to
    // what null in this case means
    println("onOffTrait supports startUpOnOff and it is null")
  } else {
    // This value is nullable and set to a value.
    println("onOffTrait supports startUpOnOff and it is set to ${onOffTrait.startUpOnOff}")
  }
} else {
  println("onOffTrait does not support startUpOnOff!")
}

特徴属性を更新する

特定の属性の値を変更したいが、トレイトのコマンドでは変更できない場合は、その属性が値の明示的な設定をサポートしている可能性があります。

属性の値を変更できるかどうかは、次の 2 つの要因によって決まります。

  • 属性は書き込み可能か
  • 属性の値は、トレイト コマンドの送信の副作用として変更されますか?

この情報は、トレイトとその属性のリファレンス ドキュメントで確認できます。

したがって、属性の値を変更する方法を指定するプロパティの組み合わせは次のとおりです。

  • 読み取り専用で、他のコマンドの影響を受けません。つまり、属性の値は変更されません。たとえば、Switch トレイトの currentPosition 属性などです。

  • 読み取り専用で、他のコマンドの影響を受けます。つまり、属性の値を変更できるのは、コマンドを送信した結果のみです。たとえば、LevelControl Matter トレイトの currentLevel 属性は読み取り専用ですが、その値は moveToLevel などのコマンドで変更できます。

  • 書き込み可能で、他のコマンドの影響を受けません。つまり、トレイトの update 関数を使用して属性の値を直接変更できますが、属性の値に影響するコマンドは存在しません。たとえば、DoorLock トレイトの WrongCodeEntryLimit 属性などです。

  • 書き込み可能で、他のコマンドの影響を受けます。つまり、トレイトの update 関数を使用して属性の値を直接変更できます。また、コマンドを送信した結果として属性の値が変更される可能性があります。たとえば、Thermostat トレイトの occupiedCoolingSetpoint 属性は書き込み可能であるだけでなく、setpointRaiseLower コマンドで更新することもできます。

update 関数を使用して属性の値を変更する例

この例は、DoorLockTrait.WrongCodeEntryLimit 属性の値を明示的に設定する方法を示しています。

属性値を設定するには、トレイトの update 関数を呼び出し、新しい値を設定するミュータ関数を渡します。最初に、トレイトが属性をサポートしていることを確認することをおすすめします。

次に例を示します。

    var doorLockDevice = home.devices().list().first { device -> device.has(DoorLock) }

    val traitFlow: Flow<DoorLock?> =
      doorLockDevice.type(DoorLockDevice).map { it.standardTraits.doorLock }.distinctUntilChanged()

    val doorLockTrait: DoorLock = traitFlow.first()!!

    if (doorLockTrait.supports(DoorLock.Attribute.wrongCodeEntryLimit)) {
      val unused = doorLockTrait.update { setWrongCodeEntryLimit(3u) }
    }

複数のコマンドを一度に送信する

Batching API を使用すると、クライアントは 1 つのペイロードで複数の Home API デバイス コマンドを送信できます。コマンドは 1 つのペイロードにバッチ処理され、並列ノードを使用して Home API の自動化を作成する方法と同様に、並列で実行されます(日の出前にブラインドを開くの例など)。ただし、Batching API では、任意の条件に応じて実行時にデバイスを動的に選択する機能など、Automation API よりも複雑で高度な動作が可能になります。

1 つのバッチのコマンドは、複数のデバイス、複数の部屋、複数の構造物にわたる複数の特徴をターゲットにできます。

コマンドをバッチで送信すると、デバイスはアクションを同時に実行できます。これは、コマンドを個別のリクエストで順番に送信する場合は実現できません。バッチ処理コマンドを使用して実現される動作により、デベロッパーは、事前定義された集計状態に合わせてデバイスのグループの状態を設定できます。

Batching API を使用する

Batching API を介してコマンドを呼び出すには、次の 3 つの基本的な手順があります。

  1. Home.sendBatchedCommands() メソッドを呼び出します。
  2. sendBatchedCommands() ブロックの本文で、バッチに含めるコマンドを指定します。
  3. 送信されたコマンドの結果を確認して、コマンドが成功したかどうかを確認します。

sendBatchedCommands() メソッドを呼び出す

Home.sendBatchedCommands() メソッドを呼び出します。このメソッドは、特別なバッチ コンテキストでラムダ式を設定します。

home.sendBatchedCommands() {

バッチコマンドを指定する

sendBatchedCommands() ブロックの本文内にバッチ処理可能なコマンドを入力します。バッチ処理可能なコマンドは、バッチ コンテキストで使用できる既存の Device API コマンドの「シャドー」バージョンであり、末尾に Batchable が追加された名前が付けられています。たとえば、LevelControl トレイトの moveToLevel() コマンドには、moveToLevelBatchable() という名前の対応するコマンドがあります。

例:

  val response1 = add(command1)

  val response2 = add(command2)

すべてのコマンドがバッチ コンテキストに追加され、実行がコンテキストから離れると、バッチは自動的に送信されます。

レスポンスは DeferredResponse<T> オブジェクトでキャプチャされます。

DeferredResponse<T> インスタンスは、Collection や定義したデータクラスなど、任意の型のオブジェクトに集めることができます。レスポンスの組み立てに選択したオブジェクトのタイプが、sendBatchedCommands() から返されます。たとえば、バッチ コンテキストは Pair で 2 つの DeferredResponse インスタンスを返すことができます。

  val (response1, response2) = homeClient.sendBatchedComamnds {
    val response1 = add(someCommandBatched(...))
    val response2 = add(someOtherCommandBatched(...))
    Pair(response1, response2)
  }

または、バッチ コンテキストは、カスタム データクラスの DeferredResponse インスタンスを返すことができます。

  // Custom data class
  data class SpecialResponseHolder(
    val response1: DeferredResponse<String>,
    val response2: DeferredResponse<Int>,
    val other: OtherResponses
  )
  data class OtherResponses(...)

各レスポンスを確認する

sendBatchedCommands() ブロックの外部でレスポンスをチェックし、対応するコマンドが成功したかどうかを判断します。これは、DeferredResponse.getOrThrow() を呼び出すことによって行われます。DeferredResponse.getOrThrow() は、次のいずれかを行います。 - 実行されたコマンドの結果を返します。 - バッチ スコープが完了していないか、コマンドが失敗した場合は、エラーをスローします。

結果を確認するのは、sendBatchedCommands() ラムダ スコープの外部でのみ行います。

たとえば、Batching API を使用して「おやすみ」シーンを設定し、家中のすべてのデバイスを夜間(全員が寝ている時間帯)用に設定するアプリを作成するとします。このアプリは、照明を消し、前後ドアをロックします。

タスクに取り組む方法の 1 つは次のとおりです。

val lightDevices: List<OnOffLightDevice>
val doorlockDevices: List<DoorLockDevice>

// Send all the commands
val responses: List<DeferredResponse<Unit>> = home.sendBatchedCommands {
  // For each light device, send a Batchable command to turn it on
  val lightResponses: List<DeferredResponse<Unit>> = lightDevices.map { lightDevice ->
    add(lightDevice.standardTraits.onOff.onBatchable())
  }

  // For each doorlock device, send a Batchable command to lock it
  val doorLockResponse: List<DeferredResponse<Unit>> = doorlockDevices.map { doorlockDevice ->
    add(doorlockDevice.standardTraits.doorLock.lockDoorBatchable())
  }

  lightResponses + doorLockResponses
}

// Check that all responses were successful
for (response in responses) {
  response.getOrThrow()
}