التحكّم في الأجهزة على 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(). وراء الكواليس، تُعدّ هذه الطريقة تعبير lambda في سياق دفعة خاص.

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()، والذي يؤدي إلى: - عرض نتيجة الأمر الذي تم تنفيذه، - أو عرض خطأ إذا لم يكتمل نطاق الدفعة أو لم ينجح الأمر.

يجب التحقّق من النتائج خارج نطاق دالة lambda في 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).

سمات سمة جزء المجموعة ومعالجة الأخطاء

يؤدي استهداف جزء من المجموعة إلى تبسيط الحالات الشائعة التي تريد فيها إرسال أمر متكرر (مثل تشغيل الأضواء أو إطفائها أو ضبط مستوى السطوع) عبر مجموعة. عند العمل مع السمات والأوامر على جزء 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(),
  )
}