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 トレイトの Toggle コマンドを使用します。このコマンドは、Google Home エコシステム データモデルで toggle() として定義されています。このメソッドは、onOff が true の場合は 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 列挙型を使用して、特定のデバイスで属性がサポートされているかどうかを確認します。

たとえば、デバイスが 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 値を許容します。これらの属性の場合、属性によって返される null が、デバイスがその値を報告していないことによるものか、属性の値が実際に null であるかどうかを判断するには、supports に加えて isNullable を使用します。

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

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

次の例は、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) }
    }

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

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

1 つのバッチ内のコマンドは、複数のストラクチャ、複数の部屋、複数のデバイスにわたる複数のトレイトをターゲットにできます。

コマンドをバッチで送信すると、デバイスはアクションを同時に実行できます。これは、コマンドを個別のリクエストで順番に送信する場合には不可能です。バッチコマンドを使用すると、デベロッパーはデバイスのグループの状態を、事前に定義された集約状態に一致するように設定できます。

Batching API を使用する

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

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

バッチコマンドを送信する

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

デバイス グループを操作する

メンバー デバイスを反復処理することで、グループのすべてのメンバーをきめ細かく制御できるだけでなく、DeviceGroup を単一のエンティティとして制御することもできます。DeviceGroup は PartsProvider インターフェースを実装します。このインターフェースは、グループ内のデバイスでサポートされている DeviceType 部分を公開します。

  • group.parts() は、グループに存在するすべてのデバイスタイプの Flow<Set<DeviceType>> を返します。
  • group.part() と group.partOrNull() は、特定のデバイスタイプ(OnOffLightDevice や DimmableLightDevice など)のフローを返します。

DeviceGroup の一部であるデバイスタイプでトレイト コマンドを呼び出すと、SDK は、基盤となる接続やトランスポート(Matter や Cloud-to-cloud など)に関係なく、その機能をサポートするグループ内の関連するすべてのメンバー デバイスにコマンドをリレーします。

グループ部品特性属性とエラー処理

グループの一部をターゲットに設定すると、グループ全体にべき等コマンド(照明のオン / オフ、明るさの設定など)を送信する一般的なケースが簡素化されます。DeviceGroup 部分でトレイトとコマンドを操作する場合は、次の動作に注意してください。

  • 動的状態属性(OnOff トレイトの onOff 属性や LevelControl トレイトの currentLevel 属性など)は、通常、グループ部分のトレイトには入力されません。グループ内のメンバー デバイスは、単一の値に結合できない異なる状態になる可能性があるためです。ただし、サポートされているコマンド(supports() で確認。調光可能なライトの LevelControl.Command.MoveToLevelWithOnOff など)と、選択した機能属性(カラーライトの ColorControl の colorTempPhysicalMinMireds や colorTempPhysicalMaxMireds など)は、アプリが UI コントロールを適切に構成できるように入力されます。
  • グループ メンバーのトレイトのすべてのコマンドがグループ パーツで使用できるわけではありません。特に、値を返すコマンドは使用できません。
  • グループ部分に対するコマンドは、基盤となる各メンバー デバイス コマンドの個別の結果を返しません。個々のコマンドの結果を検査する場合や、値を返すコマンドを呼び出す場合は、グループのメンバー デバイスを直接反復処理します。
  • DeviceGroup の一部に送信されたコマンドは、メンバー デバイス全体にわたるファンアウト全体が終了し、基盤となるすべてのコマンドの成功または失敗が収集されると完了します。コマンドが失敗する(HomeException をスローする)のは、関連付けられているすべてのメンバー デバイスでコマンドが失敗した場合、またはすべての対象デバイスが利用できない場合のみです。これは、単一コマンドの呼び出し構文に一致する単一コマンドの失敗として表示されます。少なくとも 1 つのメンバー デバイスがコマンドを実行すると、グループ コマンドは例外をスローせずに完了します。

グループの一部にコマンドを送信する

デバイス グループ内のすべての OnOffLightDevice ライトをオンにするには:

val lightPart = group.partOrNull(OnOffLightDevice).firstOrNull()
val onOffTrait = lightPart?.trait(OnOff)

if (onOffTrait?.supports(OnOff.Command.On) == true) {
  onOffTrait.on()
}

group.parts() を使用して、グループ内のすべてのデバイスタイプを検査し、複数の関連するデバイスタイプにわたってトレイトをターゲットにすることもできます(たとえば、OnOff をサポートするグループ内の照明デバイスタイプをオフにするなど)。

val lightTypes = listOf(
  OnOffLightDevice,
  DimmableLightDevice,
  ColorTemperatureLightDevice,
  ExtendedColorLightDevice,
)

val parts = group.parts().first()
val onOffTrait = lightTypes.firstNotNullOfOrNull { type ->
  parts.firstOrNull { it.factory == type }
}?.trait(OnOff)

onOffTrait?.off()

コマンドを送信する前に、グループ部分のコマンド サポートまたは読み取り機能の境界を確認できます(たとえば、調光可能なライト部分で MoveToLevelWithOnOff を確認したり、色温度ライト部分で colorTempPhysicalMinMireds と colorTempPhysicalMaxMireds を読み取ったりできます)。

val dimmableLight = group.partOrNull(DimmableLightDevice).firstOrNull()
val levelControl = dimmableLight?.trait(LevelControl)

if (
  levelControl?.supports(
    LevelControl.Command.MoveToLevelWithOnOff
  ) == true
) {
  levelControl.moveToLevelWithOnOff(
    level = 127u,
    transitionTime = null,
    optionsMask = LevelControlTrait.OptionsBitmap(),
    optionsOverride = LevelControlTrait.OptionsBitmap(),
  )
}

val colorLight = group.partOrNull(ColorTemperatureLightDevice).firstOrNull()
val colorControl = colorLight?.trait(ColorControl)
val minMireds = colorControl?.colorTempPhysicalMinMireds
val maxMireds = colorControl?.colorTempPhysicalMaxMireds

if (colorControl != null && minMireds != null && maxMireds != null) {
  val midMireds = ((minMireds.toInt() + maxMireds.toInt()) / 2).toUShort()
  colorControl.moveToColorTemperature(
    colorTemperatureMireds = midMireds,
    transitionTime = 0u,
    optionsMask = ColorControlTrait.OptionsBitmap(),
    optionsOverride = ColorControlTrait.OptionsBitmap(),
  )
}