iOS의 Structure API

Structure API는 iOS용 Home API를 통해 액세스할 수 있습니다.

Structure API를 사용하려면 먼저 GoogleHomeSDK 패키지를 앱으로 가져옵니다.

import GoogleHomeSDK

오류 처리

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()를 호출하면 가장 최근의 요소 집합이 반환됩니다.

// 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자를 초과하면 잘리고 오류가 발생하지 않습니다. 개발자는 긴 이름을 처리할 책임이 있으며, 예를 들어 이름이 잘린다는 사실을 사용자에게 알릴지 여부를 결정할 수 있습니다.

기기 그룹

기기 그룹은 단일 구조 내에서 사용자가 정의한 기기 모음을 나타냅니다. 그룹 멤버십은 Google 서비스에 유지되며 구조의 다른 구성원과 구조에 액세스할 수 있는 다른 앱에 표시됩니다.

실제 방과 마찬가지로 기기 그룹은 단일 구조에 속합니다. 하지만 방과 달리 기기 그룹은 논리적 그룹입니다. 기기는 동시에 여러 기기 그룹에 속할 수 있습니다. 기기 그룹에는 최대 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)")
}

그룹 구성원 기기와 상호작용

그룹의 구성원 기기를 반복하면 개별 구성원을 세밀하게 제어하고, 단일 요청으로 명령어를 일괄 처리하고, 구성원 간에 속성을 집계하거나 작성할 수 있습니다.

먼저 각 기기의 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)