ควบคุมอุปกรณ์ใน Android

ตรวจสอบว่าลักษณะรองรับคำสั่งหรือไม่

นอกจากนี้ คุณยังตรวจสอบการรองรับคำสั่งลักษณะได้ด้วย นอกจากนี้ ให้ใช้ฟังก์ชัน supports ระดับลักษณะเพื่อตรวจสอบว่าอุปกรณ์หนึ่งๆ รองรับคำสั่งหรือไม่

ตัวอย่างเช่น หากต้องการตรวจสอบว่าอุปกรณ์รองรับคำสั่ง 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 ซึ่งกำหนดไว้ในโมเดลข้อมูลระบบนิเวศของ Google Home เป็น toggle() วิธีนี้จะเปลี่ยน onOff เป็น false หากเป็น true หรือเป็น true หากเป็น false

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

บางคำสั่งมีอาร์กิวเมนต์ที่ไม่บังคับ ซึ่งจะอยู่หลังอาร์กิวเมนต์ที่จำเป็น

เช่น คำสั่ง step สำหรับFanControl ลักษณะ มีอาร์กิวเมนต์ที่ไม่บังคับ 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 แต่ไม่รองรับแอตทริบิวต์ที่เฉพาะเจาะจง เช่น อุปกรณ์ Cloud-to-cloud ที่แมปกับ Matter อาจไม่รองรับแอตทริบิวต์ Matter ทุกรายการ หากต้องการจัดการกรณีเช่นนี้ ให้ใช้ฟังก์ชัน supports ระดับลักษณะและ enum Attribute ของลักษณะเพื่อตรวจสอบว่าอุปกรณ์หนึ่งๆ รองรับแอตทริบิวต์หรือไม่

ตัวอย่างเช่น หากต้องการตรวจสอบว่าอุปกรณ์รองรับแอตทริบิวต์ 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!")
}

แอตทริบิวต์บางรายการเป็นค่า Null ได้ในข้อกำหนด Matter หรือสคีมา Cloud-to-cloud smart home สำหรับแอตทริบิวต์เหล่านี้ คุณสามารถระบุได้ว่าค่า null ที่แอตทริบิวต์แสดงผลนั้นเกิดจากอุปกรณ์ไม่ได้รายงานค่าดังกล่าว หรือค่าของแอตทริบิวต์เป็น null จริงๆ โดยใช้ isNullable นอกเหนือจาก 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!")
}

อัปเดตแอตทริบิวต์ลักษณะเฉพาะ

หากต้องการเปลี่ยนค่าของแอตทริบิวต์หนึ่งๆ และไม่มีคำสั่งใดของลักษณะที่ทำเช่นนั้น แอตทริบิวต์อาจรองรับการตั้งค่าอย่างชัดเจน

การเปลี่ยนแปลงค่าของแอตทริบิวต์ได้หรือไม่นั้นขึ้นอยู่กับปัจจัย 2 ประการดังนี้

  • แอตทริบิวต์เขียนได้ไหม
  • ค่าของแอตทริบิวต์จะเปลี่ยนแปลงได้ไหมเมื่อส่งคำสั่งลักษณะ

เอกสารอ้างอิงสำหรับลักษณะและแอตทริบิวต์จะมีข้อมูลนี้

ดังนั้นชุดค่าผสมของพร็อพเพอร์ตี้ที่กำหนดวิธีเปลี่ยนแปลงค่าของแอตทริบิวต์จึงมีดังนี้

  • อ่านอย่างเดียวและไม่ได้รับผลกระทบจากคำสั่งอื่นๆ ซึ่งหมายความว่าค่าของแอตทริบิวต์จะไม่เปลี่ยนแปลง เช่น แอตทริบิวต์ currentPosition ของลักษณะ Switch

  • อ่านอย่างเดียวและได้รับผลกระทบจากคำสั่งอื่นๆ ซึ่งหมายความว่าวิธีเดียวที่ค่าของแอตทริบิวต์จะเปลี่ยนแปลงได้คือการส่งคำสั่ง ตัวอย่างเช่น แอตทริบิวต์ currentLevel ของลักษณะ LevelControl Matter เป็นแบบอ่านอย่างเดียว แต่ค่าของแอตทริบิวต์สามารถเปลี่ยนแปลงได้ด้วยคำสั่งต่างๆ เช่น moveToLevel

  • เขียนได้และไม่ได้รับผลกระทบจากคำสั่งอื่นๆ ซึ่งหมายความว่าคุณสามารถเปลี่ยนค่าของแอตทริบิวต์ได้โดยตรงโดยใช้ฟังก์ชัน update ของลักษณะ แต่จะไม่มีคำสั่งที่ส่งผลต่อค่าของแอตทริบิวต์ เช่น แอตทริบิวต์ WrongCodeEntryLimit ของลักษณะ DoorLock

  • เขียนได้และได้รับผลกระทบจากคำสั่งอื่นๆ ซึ่งหมายความว่าคุณสามารถเปลี่ยนค่าของแอตทริบิวต์ได้โดยตรงโดยใช้ฟังก์ชัน update ของลักษณะ และค่าของแอตทริบิวต์อาจเปลี่ยนแปลงได้จากการส่งคำสั่ง ตัวอย่างเช่น แอตทริบิวต์ speedSetting ของ FanControlTrait สามารถเขียนลงไปได้โดยตรง แต่ก็สามารถเปลี่ยนแปลงได้โดยใช้คำสั่ง step

ตัวอย่างการใช้ฟังก์ชันอัปเดตเพื่อเปลี่ยนค่าของแอตทริบิวต์

ตัวอย่างนี้แสดงวิธีตั้งค่าแอตทริบิวต์ DoorLockTrait.WrongCodeEntryLimit อย่างชัดเจน

หากต้องการตั้งค่าแอตทริบิวต์ ให้เรียกใช้ update function ของลักษณะและส่งฟังก์ชัน Mutator ที่ตั้งค่าใหม่ให้ แนวทางปฏิบัติแนะนำคือตรวจสอบก่อนว่าลักษณะรองรับแอตทริบิวต์หรือไม่

เช่น

    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 ช่วยให้ไคลเอ็นต์ส่งคำสั่งอุปกรณ์ Home API หลายรายการในเพย์โหลดเดียวได้ ระบบจะรวมคำสั่งเป็นเพย์โหลดเดียวและดำเนินการพร้อมกัน คล้ายกับวิธีสร้างการทำงานอัตโนมัติของ Home API โดยใช้โหนดแบบขนาน เช่น ตัวอย่างเปิดมู่ลี่ก่อนพระอาทิตย์ขึ้น อย่างไรก็ตาม Batching API ช่วยให้ทำงานที่ซับซ้อนและซับซ้อนกว่า Automation API ได้ เช่น ความสามารถในการเลือกอุปกรณ์แบบไดนามิกขณะรันไทม์ตามเกณฑ์ใดก็ได้

คำสั่งในกลุ่มเดียวสามารถกำหนดเป้าหมายลักษณะหลายรายการในอุปกรณ์หลายเครื่อง ในห้องหลายห้อง และในบ้านหลายหลัง

การส่งคำสั่งเป็นกลุ่มช่วยให้อุปกรณ์ดำเนินการพร้อมกันได้ ซึ่งเป็นไปไม่ได้เลยเมื่อส่งคำสั่งตามลำดับในคำขอแยกกัน ลักษณะการทำงานที่ได้จากการใช้คำสั่งแบบกลุ่มช่วยให้นักพัฒนาแอปกำหนดสถานะของกลุ่มอุปกรณ์ให้ตรงกับสถานะรวมที่กำหนดไว้ล่วงหน้าได้

ใช้ Batching API

การเรียกใช้คำสั่งผ่าน Batching API มีขั้นตอนพื้นฐาน 3 ขั้นตอนดังนี้

  1. เรียกใช้เมธอด Home.sendBatchedCommands()
  2. ภายในเนื้อความของบล็อก sendBatchedCommands() ให้ระบุคำสั่งที่จะรวมไว้ในกลุ่ม
  3. ตรวจสอบผลลัพธ์ของคำสั่งที่ส่งเพื่อดูว่าสำเร็จหรือล้มเหลว

ส่งคำสั่งแบบกลุ่ม

เรียกใช้เมธอด Home.sendBatchedCommands() เบื้องหลังการทำงาน วิธีนี้จะตั้งค่านิพจน์ Lambda ในบริบทกลุ่มพิเศษ

home.sendBatchedCommands() {

ระบุคำสั่งแบบกลุ่ม

ในส่วนเนื้อหาของบล็อก sendBatchedCommands() ให้ป้อนคำสั่งที่รวมเป็นกลุ่มได้ คำสั่งที่จัดกลุ่มได้คือคำสั่ง Device API ที่มีอยู่ซึ่งเป็นเวอร์ชัน "Shadow" ซึ่งใช้ในบริบทกลุ่มได้ และตั้งชื่อโดยเพิ่มคำต่อท้าย Batchable ตัวอย่างเช่น คำสั่ง moveToLevel() ของลักษณะ LevelControl มีคำสั่งที่ทำงานร่วมกันชื่อ moveToLevelBatchable()

ตัวอย่าง

  val response1 = add(command1)

  val response2 = add(command2)

ระบบจะส่งกลุ่มคำสั่งโดยอัตโนมัติเมื่อเพิ่มคำสั่งทั้งหมดลงในบริบทกลุ่มคำสั่งและการดำเนินการออกจากบริบทแล้ว

ระบบจะบันทึกคำตอบไว้ในออบเจ็กต์ DeferredResponse<T>

คุณสามารถรวบรวมอินสแตนซ์ DeferredResponse<T> ไว้ในออบเจ็กต์ประเภทใดก็ได้ เช่น Collection หรือคลาสข้อมูลที่คุณกำหนด sendBatchedCommands() จะแสดงผลออบเจ็กต์ประเภทใดก็ตามที่คุณเลือกเพื่อรวบรวมคำตอบ เช่น บริบทกลุ่มสามารถแสดงผลอินสแตนซ์ DeferredResponse 2 รายการใน Pair ดังนี้

  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 เพื่อตั้งค่าฉาก "ราตรีสวัสดิ์" ซึ่งจะกำหนดค่าอุปกรณ์ทั้งหมดในบ้านสำหรับช่วงเวลากลางคืนเมื่อทุกคนหลับแล้ว แอปนี้ควรปิดไฟและล็อกประตูหน้าและประตูหลัง

วิธีหนึ่งในการทำงานนี้มีดังนี้

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)

แอตทริบิวต์ลักษณะของส่วนประกอบกลุ่มและการจัดการข้อผิดพลาด

การกำหนดเป้าหมายส่วนของกลุ่มจะช่วยลดความซับซ้อนของกรณีทั่วไปที่คุณต้องการส่งคำสั่งแบบ Idempotent (เช่น การเปิดหรือปิดไฟ หรือการตั้งค่าระดับความสว่าง) ไปยังทั้งกลุ่ม เมื่อทำงานกับลักษณะและคำสั่งในส่วน DeviceGroup โปรดคำนึงถึงลักษณะการทำงานต่อไปนี้

  • โดยทั่วไปแล้ว แอตทริบิวต์สถานะแบบไดนามิก (เช่น แอตทริบิวต์ onOff ของลักษณะ OnOff หรือแอตทริบิวต์ currentLevel ของลักษณะ LevelControl) จะไม่ป้อนข้อมูลในลักษณะของส่วนกลุ่ม เนื่องจากอุปกรณ์ที่เป็นสมาชิกในกลุ่มอาจอยู่ในสถานะที่แตกต่างกันซึ่งไม่สามารถรวมเป็นค่าเดียวได้ อย่างไรก็ตาม ระบบจะป้อนคำสั่งที่รองรับ (ตรวจสอบด้วย supports() เช่น LevelControl.Command.MoveToLevelWithOnOff ในหลอดไฟที่หรี่แสงได้) และแอตทริบิวต์ความสามารถบางอย่าง (เช่น colorTempPhysicalMinMireds และ colorTempPhysicalMaxMireds ใน ColorControl สำหรับหลอดไฟสี) เพื่อให้แอปของคุณกำหนดค่าตัวควบคุม 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(),
  )
}