Kiểm soát thiết bị trên Android

Kiểm tra xem một đặc điểm có hỗ trợ một lệnh hay không

Bạn cũng có thể kiểm tra xem một lệnh đặc điểm có được hỗ trợ hay không. Ngoài ra, hãy sử dụng hàm supports ở cấp đặc điểm để kiểm tra xem một lệnh có được hỗ trợ cho một thiết bị cụ thể hay không.

Ví dụ: để kiểm tra xem thiết bị có hỗ trợ lệnh toggle của đặc điểm Bật/Tắt hay không:

// 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")
}

Gửi lệnh đến thiết bị

Việc gửi lệnh cũng tương tự như việc đọc thuộc tính trạng thái từ một đặc điểm. Để bật hoặc tắt thiết bị, hãy sử dụng lệnh Bật/tắt của đặc điểm OnOff, được xác định trong mô hình dữ liệu hệ sinh thái Google Home là toggle(). Phương thức này thay đổi onOff thành false nếu là true, hoặc thành true nếu là false:

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

Tất cả lệnh đặc điểm đều là hàm suspend và chỉ hoàn tất khi API trả về phản hồi (chẳng hạn như xác nhận trạng thái thiết bị đã thay đổi). Các lệnh có thể trả về một ngoại lệ nếu phát hiện thấy vấn đề với luồng thực thi. Là nhà phát triển, bạn nên sử dụng khối try-catch để xử lý đúng cách các ngoại lệ này và hiển thị thông tin chi tiết cho người dùng về những trường hợp có thể xử lý lỗi. Các ngoại lệ chưa được xử lý sẽ dừng thời gian chạy của ứng dụng và có thể gây ra sự cố trong ứng dụng.

Ngoài ra, hãy sử dụng lệnh off() hoặc on() để đặt trạng thái một cách rõ ràng:

onOffTrait.off()
onOffTrait.on()

Sau khi gửi lệnh thay đổi trạng thái, khi lệnh hoàn tất, bạn có thể đọc trạng thái như mô tả trong phần Đọc trạng thái thiết bị để xử lý trạng thái đó trong ứng dụng. Ngoài ra, bạn có thể sử dụng luồng như mô tả trong phần Quan sát trạng thái, đây là phương thức ưu tiên.

Gửi lệnh có tham số

Một số lệnh có thể sử dụng tham số, chẳng hạn như các lệnh trên đặc điểm OnOff hoặc 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(),
)

Một số lệnh có các đối số không bắt buộc, nằm sau các đối số bắt buộc.

Ví dụ: lệnh step cho đặc điểm FanControl có hai đối số không bắt buộc:

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 }

Kiểm tra xem một đặc điểm có hỗ trợ một thuộc tính hay không

Một số thiết bị có thể hỗ trợ một đặc điểm Matter, nhưng không hỗ trợ một thuộc tính cụ thể. Ví dụ: một thiết bị Cloud-to-cloud được liên kết với Matter có thể không hỗ trợ mọi thuộc tính của Matter. Để xử lý các trường hợp như thế này, hãy sử dụng hàm supports ở cấp đặc điểm và enum Attribute của đặc điểm để kiểm tra xem thuộc tính có được hỗ trợ cho một thiết bị cụ thể hay không.

Ví dụ: để kiểm tra xem thiết bị có hỗ trợ thuộc tính onOff của đặc điểm Bật/Tắt hay không, hãy làm như sau:

// 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!")
}

Một số thuộc tính có thể rỗng trong quy cách Matter hoặc giản đồ Cloud-to-cloud smart home. Đối với các thuộc tính này, bạn có thể xác định xem giá trị null mà thuộc tính trả về là do thiết bị không báo cáo giá trị đó hay giá trị của thuộc tính thực sự là null, bằng cách sử dụng isNullable ngoài supports:

// 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!")
}

Cập nhật thuộc tính đặc điểm

Nếu bạn muốn thay đổi giá trị của một thuộc tính nhất định và không có lệnh nào của đặc điểm thực hiện việc này, thì thuộc tính đó có thể hỗ trợ việc đặt giá trị một cách rõ ràng.

Việc có thể thay đổi giá trị của một thuộc tính hay không phụ thuộc vào hai yếu tố:

  • Thuộc tính có thể ghi được không?
  • Giá trị của thuộc tính có thể thay đổi do tác dụng phụ của việc gửi lệnh đặc điểm không?

Tài liệu tham khảo về các đặc điểm và thuộc tính của đặc điểm cung cấp thông tin này.

Do đó, các tổ hợp thuộc tính quyết định cách thay đổi giá trị của một thuộc tính là:

  • Chỉ đọc và không bị ảnh hưởng bởi các lệnh khác. Điều này có nghĩa là giá trị của thuộc tính không thay đổi. Ví dụ: thuộc tính currentPosition của đặc điểm Switch.

  • Chỉ đọc và bị ảnh hưởng bởi các lệnh khác. Điều này có nghĩa là cách duy nhất để thay đổi giá trị của thuộc tính là gửi lệnh. Ví dụ: thuộc tính currentLevel của đặc điểm LevelControl Matter là thuộc tính chỉ có thể đọc, nhưng giá trị của thuộc tính này có thể thay đổi bằng các lệnh như moveToLevel.

  • Có thể ghi và không bị ảnh hưởng bởi các lệnh khác. Điều này có nghĩa là bạn có thể thay đổi trực tiếp giá trị của thuộc tính bằng cách sử dụng hàm update của đặc điểm, nhưng không có lệnh nào ảnh hưởng đến giá trị của thuộc tính. Ví dụ: thuộc tính WrongCodeEntryLimit của đặc điểm DoorLock.

  • Có thể ghi và bị ảnh hưởng bởi các lệnh khác. Điều này có nghĩa là bạn có thể thay đổi trực tiếp giá trị của thuộc tính bằng cách sử dụng hàm update của đặc điểm và giá trị của thuộc tính có thể thay đổi do gửi lệnh. Ví dụ: thuộc tính speedSetting của FanControlTrait có thể được ghi trực tiếp, nhưng cũng có thể thay đổi bằng lệnh step.

Ví dụ về cách sử dụng hàm cập nhật để thay đổi giá trị của thuộc tính

Ví dụ này cho thấy cách đặt giá trị của thuộc tính DoorLockTrait.WrongCodeEntryLimit một cách rõ ràng.

Để đặt giá trị thuộc tính, hãy gọi hàm update của đặc điểm và truyền cho hàm đó một hàm biến đổi để đặt giá trị mới. Bạn nên xác minh rằng đặc điểm đó hỗ trợ một thuộc tính trước.

Ví dụ:

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

Gửi nhiều lệnh cùng lúc

Batching API cho phép ứng dụng gửi nhiều lệnh thiết bị Home API trong một tải trọng. Các lệnh được gộp thành một tải trọng duy nhất và thực thi song song, tương tự như cách người dùng có thể tạo một quy trình tự động hoá bằng Home API bằng cách sử dụng nút song song, chẳng hạn như ví dụ Mở rèm trước khi mặt trời mọc. Tuy nhiên, Batching API cho phép các hành vi phức tạp và tinh vi hơn Automation API, chẳng hạn như khả năng chọn thiết bị một cách linh động trong thời gian chạy theo bất kỳ tiêu chí nào.

Các lệnh trong một lô có thể nhắm đến nhiều đặc điểm trên nhiều thiết bị, trong nhiều phòng, trong nhiều cấu trúc.

Việc gửi lệnh theo lô cho phép các thiết bị thực hiện hành động cùng lúc, điều này không thực sự khả thi khi các lệnh được gửi tuần tự trong các yêu cầu riêng biệt. Hành vi đạt được khi sử dụng lệnh theo lô cho phép nhà phát triển đặt trạng thái của một nhóm thiết bị sao cho khớp với trạng thái tổng hợp được xác định trước.

Sử dụng Batching API

Có 3 bước cơ bản để gọi lệnh thông qua Batching API:

  1. Gọi phương thức Home.sendBatchedCommands().
  2. Trong phần nội dung của khối sendBatchedCommands(), hãy chỉ định các lệnh cần đưa vào lô.
  3. Kiểm tra kết quả của các lệnh đã gửi để xem các lệnh đó có thành công hay không.

Gửi lệnh theo lô

Gọi phương thức Home.sendBatchedCommands(). Trong nền, phương thức này thiết lập một biểu thức lambda trong một ngữ cảnh lô đặc biệt.

home.sendBatchedCommands() {

Chỉ định lệnh theo lô

Trong phần nội dung của khối sendBatchedCommands(), hãy điền các lệnh có thể tạo lô. Lệnh có thể tạo lô là phiên bản "bóng" của các lệnh API thiết bị hiện có, có thể dùng trong bối cảnh lô và được đặt tên bằng hậu tố Batchable. Ví dụ: lệnh moveToLevel() của đặc điểm LevelControl có một lệnh tương ứng tên là moveToLevelBatchable().

Ví dụ:

  val response1 = add(command1)

  val response2 = add(command2)

Lô sẽ tự động được gửi sau khi tất cả lệnh được thêm vào bối cảnh lô và quá trình thực thi đã rời khỏi bối cảnh.

Phản hồi được ghi lại trong các đối tượng DeferredResponse<T>.

Bạn có thể tập hợp các thực thể DeferredResponse<T> vào một đối tượng thuộc bất kỳ loại nào, chẳng hạn như Collection hoặc một lớp dữ liệu mà bạn xác định. Bất kể loại đối tượng nào bạn chọn để tập hợp các phản hồi đều là loại đối tượng được sendBatchedCommands() trả về. Ví dụ: bối cảnh lô có thể trả về hai thực thể DeferredResponse trong một Pair:

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

Ngoài ra, bối cảnh lô có thể trả về các phiên bản DeferredResponse trong một lớp dữ liệu tuỳ chỉnh:

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

Kiểm tra từng câu trả lời

Bên ngoài khối sendBatchedCommands(), hãy kiểm tra các phản hồi để xác định xem lệnh tương ứng thành công hay không thành công. Bạn có thể thực hiện việc này bằng cách gọi DeferredResponse.getOrThrow(). Thao tác này sẽ: – trả về kết quả của lệnh đã thực thi, – hoặc, nếu phạm vi lô chưa hoàn tất hoặc lệnh không thành công, thì sẽ gửi một lỗi.

Bạn chỉ nên kiểm tra kết quả bên ngoài phạm vi lambda sendBatchedCommands().

Ví dụ

Giả sử bạn muốn tạo một ứng dụng sử dụng API phân lô để thiết lập cảnh "chúc ngủ ngon" nhằm định cấu hình tất cả thiết bị trong nhà cho ban đêm, khi mọi người đang ngủ. Ứng dụng này sẽ tắt đèn và khoá cửa trước và cửa sau.

Sau đây là một cách để thực hiện nhiệm vụ này:

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

Điều khiển một nhóm thiết bị

Ngoài việc kiểm soát chi tiết từng thành viên của một nhóm bằng cách lặp lại qua các thiết bị thành viên, bạn có thể kiểm soát DeviceGroup dưới dạng một thực thể duy nhất. DeviceGroup triển khai giao diện PartsProvider, cho thấy các phần DeviceType được các thiết bị trong nhóm hỗ trợ:

  • group.parts() trả về một Flow<Set<DeviceType>> của tất cả các loại thiết bị có trong nhóm.
  • group.part() và group.partOrNull() trả về một luồng cho một loại thiết bị cụ thể, chẳng hạn như OnOffLightDevice hoặc DimmableLightDevice.

Khi bạn gọi một lệnh đặc điểm trên một loại thiết bị thuộc DeviceGroup, SDK sẽ chuyển tiếp lệnh đó đến tất cả thiết bị thành viên có liên quan trong nhóm hỗ trợ chức năng đó, bất kể khả năng kết nối hoặc phương thức truyền tải cơ bản của các thiết bị đó (chẳng hạn như Matter hoặc Cloud-to-cloud).

Thuộc tính đặc điểm của bộ phận nhóm và cách xử lý lỗi

Việc nhắm đến một phần của nhóm sẽ đơn giản hoá các trường hợp phổ biến mà bạn muốn gửi một lệnh không thay đổi (chẳng hạn như bật hoặc tắt đèn, hoặc đặt mức độ sáng) cho một nhóm. Khi làm việc với các đặc điểm và lệnh trên một phần DeviceGroup, hãy lưu ý các hành vi sau:

  • Các thuộc tính trạng thái động (chẳng hạn như thuộc tính onOff của đặc điểm OnOff hoặc thuộc tính currentLevel của đặc điểm LevelControl) thường không được điền sẵn trên đặc điểm của một phần nhóm vì các thiết bị thành viên trong một nhóm có thể ở các trạng thái khác nhau và không thể kết hợp thành một giá trị duy nhất. Tuy nhiên, các lệnh được hỗ trợ (được kiểm tra bằng supports(), chẳng hạn như LevelControl.Command.MoveToLevelWithOnOff trên đèn có thể điều chỉnh độ sáng) và một số thuộc tính chức năng (chẳng hạn như colorTempPhysicalMinMireds và colorTempPhysicalMaxMireds trên ColorControl cho đèn màu) sẽ được điền sẵn để ứng dụng của bạn có thể định cấu hình các thành phần điều khiển trên giao diện người dùng một cách phù hợp.
  • Không phải lệnh nào về đặc điểm của thành viên nhóm cũng dùng được trên một phần của nhóm. Cụ thể, các lệnh trả về giá trị sẽ không dùng được.
  • Lệnh trên một phần của nhóm không trả về kết quả riêng lẻ cho từng lệnh của thiết bị thành viên cơ bản. Để kiểm tra kết quả của từng lệnh hoặc gọi các lệnh trả về giá trị, hãy lặp lại trực tiếp qua các thiết bị thành viên của nhóm.
  • Các lệnh được gửi đến một phần trên DeviceGroup sẽ hoàn tất sau khi toàn bộ quá trình phân phối trên các thiết bị thành viên kết thúc và tất cả các lệnh thành công hoặc không thành công đều được thu thập. Một lệnh chỉ không thành công (gửi một HomeException) nếu lệnh đó không thành công đối với mọi thiết bị thành viên được liên kết hoặc nếu tất cả thiết bị mục tiêu đều không hoạt động, xuất hiện dưới dạng một lỗi lệnh duy nhất khớp với cú pháp gọi lệnh đơn. Nếu có ít nhất một thiết bị thành viên thực thi lệnh, thì lệnh nhóm sẽ hoàn tất mà không gửi một trường hợp ngoại lệ.

Gửi lệnh đến một phần của nhóm

Cách bật tất cả đèn OnOffLightDevice trong một nhóm thiết bị:

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

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

Bạn cũng có thể kiểm tra tất cả các loại thiết bị có trong một nhóm bằng cách sử dụng group.parts() để nhắm đến một đặc điểm trên nhiều loại thiết bị có liên quan (ví dụ: tắt mọi loại thiết bị đèn trong nhóm có hỗ trợ 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()

Bạn có thể kiểm tra khả năng hỗ trợ lệnh hoặc đọc giới hạn chức năng trên một phần của nhóm trước khi gửi lệnh (ví dụ: kiểm tra MoveToLevelWithOnOff trên một phần đèn có thể điều chỉnh độ sáng hoặc đọc colorTempPhysicalMinMireds và colorTempPhysicalMaxMireds trên một phần đèn có nhiệt độ màu):

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