iOS で API を構造化する

Structure API には、iOS 向け Google Home API を介してアクセスできます。

Structure API を使用するには、まず GoogleHomeSDK パッケージをアプリにインポートします。

import GoogleHomeSDK

エラー処理

Google Home API の一部のメソッドは HomeError をスローするため、do-catch ブロックを使用して、これらの呼び出しで HomeError をキャッチすることをおすすめします。

HomeError を処理する際は、code フィールドと message フィールドをチェックして、問題の内容を確認します。

処理されないエラーがあると、アプリがクラッシュします。

詳細については、エラー処理をご覧ください。

Structure API

Home はホームグラフを表し、Structure API のエントリ ポイントです。ストラクチャ、部屋、デバイスへの参照を提供します。

Structure は、ホームグラフ内のストラクチャを表します。id や name などの構造メタデータへのアクセスを提供します。

structures() を使用して、アカウント内のすべてのストラクチャを取得します。構造は Query の形式で返されます。この形式では、次の方法でデータを使用できます。

API 説明
stream() 変更が発生するたびに各オブジェクトを個別に発行する Publisher を返します。
batched() 現在の結果をオブジェクトの Set として出力する Publisher を返します。出力される各 Set は、オブジェクト グラフの現在の状態を表します。
list() 現在の結果をオブジェクトの Set として返します。

structures().list() 呼び出しは、有効な構造セットをすぐに返さない場合があります。アプリがリアクティブで、stream() を呼び出してすべてのストラクチャの変更をサブスクライブして UI を駆動する場合、最終的に有効なストラクチャのリストが返されます。空のストラクチャ リストが返される状況は他にもあります。たとえば、ユーザーのスマートフォンが接続性を失った場合や、ユーザーがアプリへの権限を取り消した場合などです。アプリでこれらのケースを処理する必要があります。

@Published public private(set) var structures: [Structure] = []
private var structuresCancellable: AnyCancellable?

  self.structuresCancellable = home
    .structures()
    .batched()
    .receive(on: DispatchQueue.main)
    .map { Array($0) }
    .catch {
      // Failed to load structures
      return Just([Structure]())
    }
    .assign(to: \.structures, on: self)

ストラクチャの呼び出し例

ストラクチャのセットを取得する

Query<Structure> で list() を呼び出すと、最新の要素の Set が返されます。

// Get a stream of all structures accessible to the user
let allStructuresChanges = self.home.structures()
let allStructures = try? await allStructuresChanges.list()

リアクティブ アプリを設計する場合は、list() ではなく batched() 呼び出しと stream() 呼び出しを使用します。これらは、ホームグラフが変更されると自動的にデータを生成するためです。

ストラクチャのプロパティを取得する

ストラクチャのリストを取得したら、それらのプロパティにアクセスできます。

// Get a stream of changes taking place on a structure.
let structureChanges = try await home.structures().list().filter { $0.id == structureID }

// Get a snapshot of the structure.
let structure = structureChanges.first!

// Get structure properties
print("id \(structure.id) ")
print("name \(structure.name) ")

名前でストラクチャを検索する

ストラクチャの名前がわかっている場合は、name プロパティを使用してアクセスすることもできます。

do {
  structure1 = try await home.structures().list().first(where: { $0.name == "Main House" })
} catch let _ as HomeError {
  // Code for handling the exception
}

そこから、各ストラクチャのプロパティ、部屋、デバイスにアクセスできます。

複数のストラクチャを操作する

複数のストラクチャを使用するには、各ストラクチャへの参照を個別に取得します。

var structure1: Structure!
var structure2: Structure!
do {
  structure1 = try await home.structures().list().first(where: { $0.name == "Main House" })
} catch let _ as HomeError {
  // Code for handling the exception
}
do {
  structure2 = try await home.structures().list().first(where: { $0.name == "Guest Cottage" })
} catch let _ as HomeError {
  // Code for handling the exception
}

Rooms

部屋にはデバイスのグループが含まれます。部屋は常にストラクチャの一部であり、ストラクチャには複数の部屋を含めることができます。ストラクチャから部屋を削除しても、その部屋にあるデバイスはストラクチャから削除されません。ただし、部屋が削除されると、その部屋のデバイスは割り当て解除されます。

Home.rooms() を使用してアカウント内のすべての部屋を取得し、roomID = device.roomID を使用して各部屋の対応するデバイスを表示します。

self.home.rooms().batched()
  .combineLatest(self.home.devices().batched())
  .receive(on: DispatchQueue.main)
  .catch { error in
    // Failed to load rooms and devices
    return Just((Set<Room>(), Set<HomeDevice>()))
  }
  .map { rooms, devices in
    var devicesByRoom = [Room: [HomeDevice]]()
    for room in rooms where room.structureID == currentStructureID {
      devicesByRoom[room] = devices.filter { $0.roomID == room.id }
    }
    return devicesByRoom
  }.assign(to: &self.$devicesByRoom)

サンプルルームの通話

会議室のリストを取得する

Home クラスを使用すると、部屋のリストを取得して、そのプロパティにアクセスできます。

let allRoomsChanges = self.home.rooms()
let allRooms = try await allRoomsChanges.list()
let room = allRooms.first!
XCTAssertTrue(allRooms.contains(room))

print("id \(room.id) ")
print("name \(room.name) ")

チャットルームを作成する

Structure で新しい部屋を作成するには:

let testName = "Test Room Name"
var newRoom: Room!
do {
  newRoom = try await structure.createRoom(name: testName)
  XCTAssertNotNil(newRoom)
} catch let _ as HomeError {
  // Code for handling the exception
}

部屋を削除する

または、部屋を削除することもできます。

val roomToDelete = structure.rooms().list().filter { it.name == "room_id1" }.firstOrNull()
    structure.deleteRoom(roomToDelete!!)

ID を使用してルームを削除することもできます。

let roomToDelete = allRooms.first(where: { $0.id == room.id })
if let roomToDelete1 = roomToDelete {
  do {
    try await structure.deleteRoom(roomToDelete1)
  } catch let _ as HomeError {
    // Code for handling the exception
  }
}

デバイスが割り当てられている部屋を削除しても、デバイスはストラクチャに残りますが、部屋には割り当てられなくなります。

デバイスを別の部屋に移動する

Structure を使用して、デバイスを別の部屋に移動することもできます。

do {
  try await structure.move(device: light, to: room)
} catch let _ as HomeError {
  // Code for handling the exception
}

部屋の名前を変更する

setName(_:) メソッドを呼び出して、部屋の名前を変更します。

let updatedRoom = try await theRoom.setName("new room name")

部屋の名前を変更しても、元の Room 構造体は変更されず、変更は返された更新済みの Room オブジェクトに反映されます。

名前が 60 Unicode コードポイント(文字)の上限を超えると、切り捨てられます。エラーはスローされません。長い名前の処理はデベロッパーの責任であり、たとえば、名前が切り捨てられることをユーザーに通知するかどうかを決定できます。

デバイス グループ

デバイス グループは、1 つのストラクチャ内のデバイスのユーザー定義コレクションを表します。グループ メンバーシップは Google サービスに保持され、ストラクチャの他のメンバーや、ストラクチャにアクセスできる他のアプリに表示されます。

物理的な部屋と同様に、デバイス グループは 1 つのストラクチャに属します。ただし、部屋とは異なり、デバイス グループは論理的なグループです。デバイスは、複数のデバイス グループに同時に属することができます。デバイス グループには最大 100 台のデバイスを含めることができます。

デバイス グループは DeviceGroup エンティティで表されます。これは HomeDevice とは異なります。デバイス グループを操作するには、Home.configure で ReferencedAutomationTypes を構成するときに、関連するデバイスタイプとトレイトを、GroupTrait、GroupManagementTrait、GroupMembershipTrait トレイトとともに含めます。

グループ内のデバイスを操作するには、メンバー デバイスを直接反復処理するか(グループ メンバー デバイスを操作するを参照)、DeviceGroup のデバイスタイプ部分をターゲットにします(デバイス グループを操作するを参照)。

デバイス グループのリストを取得する

家にあるすべてのデバイス グループをクエリするには:

let groups = try await home.entities(DeviceGroup.self).list()
for group in groups {
  let groupTrait = await group.traits.get(Google.GroupTrait.self)
  let name = groupTrait?.attributes.name ?? "Device Group"
  print("Found group: \(name) with ID: \(group.id)")
}

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

グループのメンバー デバイスを反復処理することで、個々のメンバーをきめ細かく制御できます。また、1 つのリクエストでコマンドをバッチ処理したり、メンバー間で属性を集計または書き込んだりできます。

まず、各デバイスの GroupMembershipTrait でターゲット グループ ID を確認して、家にあるデバイスをフィルタします。

let allDevices = try await home.devices().list()
var members: [HomeDevice] = []
for device in allDevices {
  if let membership = await device.traits.get(
    Google.GroupMembershipTrait.self
  ),
  membership.attributes.groupObjectIdsArray.contains(group.id) {
    members.append(device)
  }
}

グループ内のすべての互換性のあるメンバーに対して、トレイトのコマンドを含む単一のバッチ リクエストを送信するには:

var onOffTraits: [Matter.OnOffTrait] = []
for device in members {
  if let light = await device.types.get(OnOffLightDeviceType.self),
     let onOff = light.matterTraits.onOffTrait {
    onOffTraits.append(onOff)
  }
}

let traitsToUse = onOffTraits
let responses = try await home.sendBatchedCommands { batcher in
  return try traitsToUse.map { batcher.add(try $0.onBatchable()) }
}

for response in responses {
  try response.getOrThrow()
}

また、グループ内の一致するすべてのメンバー デバイスで属性を同時に更新し(たとえば、すべての DimmableLightDeviceType メンバーで onTransitionTime を 1 に設定)、期限内に完了するのを待つこともできます。

try await withThrowingTaskGroup(of: Void.self) { taskGroup in
  taskGroup.addTask {
    try await Task.sleep(for: .seconds(5))
    throw CancellationError()
  }

  for device in members {
    taskGroup.addTask {
      guard let light = await device.types.get(DimmableLightDeviceType.self),
            let levelControl = light.matterTraits.levelControlTrait,
            levelControl.attributes.$onTransitionTime.isSupported
      else { return }
      _ = try await levelControl.update {
        $0.setOnTransitionTime(1)
      }
    }
  }

  for _ in 0..<members.count {
    try await taskGroup.next()
  }
  taskGroup.cancelAll()
}

グループのメンバー間で属性を集計するには(グループ内のすべての色温度ライトで最小の colorTempPhysicalMaxMireds を見つけるなど):

var maxMiredsValues: [UInt16] = []
for device in members {
  if let light = await device.types.get(ColorTemperatureLightDeviceType.self),
     let mireds = light.matterTraits.colorControlTrait?.attributes
       .colorTempPhysicalMaxMireds {
    maxMiredsValues.append(mireds)
  }
}
let minMaxMireds = maxMiredsValues.min()

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

構造体で GroupManagementTrait を使用して、デバイス グループを作成します。

let groupsManagementTrait =
  try await structure.traits(Google.GroupManagementTrait)
let _ = try await groupsManagementTrait.createUserDefinedGroup(
  withName: "Kitchen Lights",
  withDevices: [light1, light2]
)

グループ メンバーを管理する

既存のグループにメンバーを追加するには:

let groupManagementTrait =
  try await structure.traits(Google.GroupManagementTrait.self)
groupManagementTrait.addGroupMembers(
  groupObjectId: group.id,
  memberDeviceObjectIds = [light3.id],
)

グループからメンバーを削除するには:

let groupManagementTrait =
  try await structure.traits(Google.GroupManagementTrait.self)
_ = try await groupManagementTrait.removeGroupMembers(
  groupObjectId: group.id,
  memberDeviceObjectIdsArray: Array(removedIDs)
)

デバイス グループの名前を変更する

デバイス グループの名前を変更するには、GroupTrait の name フィールドを更新します。

let groupTrait = group.traits.get(Google.GroupTrait.self)
_ = try await groupTrait.update { $0.name = "Movie Corner" }

デバイス グループを削除する

ストラクチャからデバイス グループを削除するには:

let groupManagementTrait =
  try await structure.traits.get(Google.GroupManagementTrait.self)

_ = try await groupManagementTrait.deleteGroup(groupObjectId: group.id)