Android でデバイスを操作する

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

サポートは、トレイト コマンドでも確認できます。また、trait レベルの 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 トレイトの Toggle コマンドを使用します。このコマンドは、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 トレイトのパラメータなどを使用する場合があります。

offWithEffect

// Turn off the light using the DyingLight effect.
onOffTrait.offWithEffect(
  effectIdentifier = OnOffTrait.EffectIdentifierEnum.DyingLight,
  effectVariant = 0u,
)

moveToLevel

// 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 列挙型を使用して、特定のデバイスで属性がサポートされているかどうかを確認します。

たとえば、デバイスが OnOff トレイトの 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 関数を使用して属性の値を直接変更できます。また、コマンドを送信した結果として属性の値が変更されることもあります。たとえば、FanControlTraitspeedSetting 属性は直接書き込むことができますが、step コマンドを使用して変更することもできます。

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

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

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

次に例を示します。

    val 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) }
    }

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

バッチ処理 API を使用すると、クライアントは複数の Home API デバイス コマンドを 1 つのペイロードで送信できます。コマンドは 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() を呼び出すことで行われます。この関数は、実行されたコマンドの結果を返すか、バッチ スコープが完了していないかコマンドが失敗した場合はエラーをスローします。

結果は 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()
}