کنترل دستگاه‌ها در 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 هستند و فقط زمانی تکمیل می‌شوند که پاسخی ازسوی میانای برنامه‌سازی کاربردی برگردانده شود (مثلاً تأیید تغییر وضعیت دستگاه). اگر مشکلی در جریان اجرا شناسایی شود، ممکن است فرمان‌ها استثنایی را برگردانند. به‌عنوان توسعه‌دهنده، باید از بلوک 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 دو شناسه اختیاری دارد:

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 در سطح مشخصه و شمارش 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!")
}

برخی‌از مشخصه‌ها در مشخصات 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!")
}

به‌روزرسانی مشخصه‌های ویژگی

اگر می‌خواهید مقدار مشخصه خاصی را تغییر دهید و هیچ‌کدام از فرمان‌های ویژگی این کار را انجام نمی‌دهد، ممکن است مشخصه از تنظیم صریح مقدارش پشتیبانی کند.

اینکه مقدار مشخصه‌ای قابل‌تغییر باشد یا نه به دو عامل بستگی دارد:

  • آیا مشخصه قابل‌نوشتن است؟
  • آیا مقدار مشخصه می‌تواند به‌عنوان اثر جانبی ارسال فرمان ویژگی تغییر کند؟

سند مرجع برای ویژگی‌ها و مشخصه‌های آن‌ها این اطلاعات را ارائه می‌دهد.

بنابراین، ترکیب ویژگی‌هایی که تعیین می‌کنند مقدار مشخصه چگونه ممکن است تغییر کند عبارت‌اند از:

  • فقط خواندنی است و تحت تأثیر فرمان‌های دیگر قرار نمی‌گیرد. این یعنی مقدار مشخصه تغییر نمی‌کند. برای مثال، مشخصه currentPosition از ویژگی Switch.

  • فقط خواندنی و تحت تأثیر فرمان‌های دیگر. این یعنی تنها راهی که مقدار مشخصه می‌تواند تغییر کند، درنتیجه ارسال فرمان است. برای مثال، مشخصه currentLevel از ویژگی LevelControl Matter فقط خواندنی است، اما مقدار آن را می‌توان با فرمان‌هایی مثل moveToLevel تغییر داد.

  • قابل‌نوشتن است و تحت تأثیر فرمان‌های دیگر قرار نمی‌گیرد. این یعنی می‌توانید مستقیماً مقدار مشخصه را بااستفاده از تابع update ویژگی تغییر دهید، اما هیچ فرمانی وجود ندارد که بر مقدار مشخصه تأثیر بگذارد. برای مثال، مشخصه WrongCodeEntryLimit از ویژگی DoorLock.

  • قابل‌نوشتن و تحت‌تأثیر فرمان‌های دیگر. این یعنی می‌توانید مستقیماً مقدار مشخصه را بااستفاده از تابع update ویژگی تغییر دهید، و مقدار مشخصه می‌تواند درنتیجه ارسال فرمان تغییر کند. برای مثال، مشخصه speedSetting از FanControlTrait می‌تواند مستقیماً نوشته شود، اما بااستفاده از فرمان step نیز قابل‌تغییر است.

نمونه استفاده از تابع به‌روزرسانی برای تغییر مقدار مشخصه

این مثال نشان می‌دهد که چگونه می‌توان مقدار مشخصه 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 به کارخواه اجازه می‌دهد چندین فرمان دستگاه Home APIs را در یک بار داده ارسال کند. فرمان‌ها در یک محموله واحد دسته‌بندی می‌شوند و به‌صورت موازی اجرا می‌شوند، مشابه نحوه ساختن خودکارسازی Home API بااستفاده از گره موازی، مانند مثال باز کردن کرکره‌ها قبل‌از طلوع آفتاب. بااین‌حال، Batching API درمقایسه با Automation API امکان رفتارهای پیچیده‌تر و پیشرفته‌تری را فراهم می‌کند، مانند توانایی انتخاب پویای دستگاه‌ها در زمان اجرا براساس هر معیاری.

فرمان‌های موجود در یک دسته می‌توانند چندین ویژگی را در چندین دستگاه، در چندین اتاق، در چندین ساختار هدف قرار دهند.

ارسال فرمان‌ها به‌صورت دسته‌ای به دستگاه‌ها امکان می‌دهد کنش‌ها را به‌طور هم‌زمان انجام دهند، که وقتی فرمان‌ها به‌صورت متوالی در درخواست‌های جداگانه ارسال می‌شوند، واقعاً امکان‌پذیر نیست. رفتار حاصل از استفاده از فرمان‌های دسته‌ای به توسعه‌دهنده امکان می‌دهد وضعیت گروهی از دستگاه‌ها را طوری تنظیم کند که با وضعیت مجموع ازپیش تعیین‌شده مطابقت داشته باشد.

استفاده از Batching API

سه مرحله اصلی برای فراخوانی فرمان‌ها ازطریق Batching API وجود دارد:

  1. روش Home.sendBatchedCommands() را فراخوانی کنید.
  2. در بدنه بلوک sendBatchedCommands()، فرمان‌هایی را که باید در دسته گنجانده شود مشخص کنید.
  3. نتایج فرمان‌های ارسال‌شده را بررسی کنید تا ببینید موفقیت‌آمیز بوده‌اند یا ناموفق.

ارسال فرمان‌های دسته‌ای

روش Home.sendBatchedCommands() را فراخوانی کنید. در پشت صحنه، این روش یک عبارت لامبدا را در یک بافت دسته‌ای ویژه راه‌اندازی می‌کند.

home.sendBatchedCommands() {

مشخص کردن فرمان‌های دسته‌ای

در بدنه بلوک sendBatchedCommands()، فرمان‌های دسته‌ای را وارد کنید. فرمان‌های دسته‌ای نسخه‌های «سایه» فرمان‌های موجود Device API هستند که می‌توانند در بافت دسته‌ای استفاده شوند و با پسوند اضافه‌شده Batchable نام‌گذاری می‌شوند. برای مثال، فرمان moveToLevel() در مشخصه LevelControl همتایی به‌نام moveToLevelBatchable() دارد.

مثال:

  val response1 = add(command1)

  val response2 = add(command2)

وقتی همه فرمان‌ها به بافت دسته اضافه شد و اجرا بافت را ترک کرد، دسته به‌طور خودکار ارسال می‌شود.

پاسخ‌ها در اشیاء DeferredResponse<T> ضبط می‌شوند.

نمونه‌های DeferredResponse<T> را می‌توان در شیئی از هر نوعی، مثل Collection، یا کلاس داده‌ای که تعریف می‌کنید، جمع‌آوری کرد. هر نوع شیئی که برای جمع‌آوری پاسخ‌ها انتخاب کنید، همان چیزی است که توسط sendBatchedCommands() برگردانده می‌شود. برای مثال، بافت دسته‌ای می‌تواند دو نمونه DeferredResponse را در یک 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() حوزه lambda را بررسی کنید.

مثال

فرض کنید می‌خواهید برنامه‌ای بسازید که از 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 است فراخوانی می‌کنید، کیت توسعه نرم‌افزار فرمان را به همه دستگاه‌های عضو مرتبط در گروه که از آن قابلیت پشتیبانی می‌کنند رله می‌کند، صرف‌نظر از اتصال یا انتقال زیربنایی آن‌ها (مانند Matter یا Cloud-to-cloud).

مشخصه‌های ویژگی بخش گروه و رسیدگی به خطا

هدف‌یابی بخش گروه، موارد رایجی را که می‌خواهید فرمان یکسان (مانند روشن یا خاموش کردن چراغ‌ها یا تنظیم سطح روشنایی) را در سراسر گروه ارسال کنید، ساده می‌کند. هنگام کار با ویژگی‌ها و فرمان‌ها در بخش DeviceGroup، رفتارهای زیر را به‌خاطر بسپارید:

  • مشخصه‌های حالت پویا (مثل مشخصه onOff از ویژگی OnOff یا مشخصه currentLevel از ویژگی LevelControl) معمولاً در ویژگی بخش گروه پر نمی‌شوند زیرا دستگاه‌های عضو در یک گروه می‌توانند در حالت‌های مختلفی باشند که نمی‌توان آن‌ها را در یک مقدار واحد ترکیب کرد. بااین‌حال، فرمان‌های پشتیبانی‌شده (بررسی‌شده با supports()، مانند LevelControl.Command.MoveToLevelWithOnOff در چراغ کم‌نورشدنی) و ویژگی‌های قابلیت انتخاب‌شده (مانند colorTempPhysicalMinMireds و colorTempPhysicalMaxMireds در ColorControl برای چراغ‌های رنگی) پر می‌شوند تا برنامه‌تان بتواند کنترل‌های میانای کاربر را به‌طور مناسب پیکربندی کند.
  • همه فرمان‌های مربوط به ویژگی‌های اعضای گروه در بخش گروه دردسترس نیست. به‌ویژه، فرمان‌هایی که مقادیر را برمی‌گردانند دردسترس نیستند.
  • فرمان در بخش گروه نتایج جداگانه‌ای برای هر فرمان دستگاه عضو زیرین برنمی‌گرداند. برای بررسی نتایج فرمان‌های تکی یا فراخوانی فرمان‌هایی که مقادیر را برمی‌گردانند، مستقیماً دستگاه‌های عضو گروه را تکرار کنید.
  • فرمان‌های ارسال‌شده به بخشی در DeviceGroup پس‌از تکمیل کل توزیع در دستگاه‌های عضو و جمع‌آوری همه موفقیت‌ها یا شکست‌های فرمان‌های اساسی تکمیل می‌شود. فرمان فقط زمانی ناموفق می‌شود (HomeException را برمی‌گرداند) که فرمان برای همه دستگاه‌های عضو مرتبط ناموفق شود یا اگر همه دستگاه‌های هدف دردسترس نباشند، که به‌صورت یک فرمان ناموفق تکی مطابق با نحو فراخوانی تک‌فرمانی ظاهر می‌شود. اگر حداقل یک دستگاه عضو، فرمان را اجرا کند، فرمان گروهی بدون ایجاد استثنا تکمیل می‌شود.

ارسال فرمان به بخش گروه

برای روشن کردن همه چراغ‌های 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(),
  )
}