שליטה במכשירים ב-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")
}

שליחת פקודה למכשיר

שליחת פקודה דומה לקריאת מאפיין מצב מתכונה. כדי להפעיל או להשבית את המכשיר, משתמשים בפקודה Toggle של מאפיין 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 trait יש שני ארגומנטים אופציונליים:

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

חלק מהמאפיינים יכולים לקבל ערך 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!")
}

עדכון מאפייני התכונות

אם רוצים לשנות את הערך של מאפיין מסוים, ואף אחת מהפקודות של התכונה לא עושה זאת, יכול להיות שהמאפיין תומך בהגדרה מפורשת של הערך שלו.

האפשרות לשנות את הערך של מאפיין תלויה בשני גורמים:

  • האם אפשר לכתוב למאפיין?
  • האם הערך של המאפיין יכול להשתנות כתוצאה משליחת פקודת מאפיין?

המידע הזה מופיע במסמכי העזר בנושא מאפיינים והמאפיינים שלהם.

לכן, השילובים של מאפיינים שמכתיבים איך אפשר לשנות את הערך של מאפיין הם:

  • לקריאה בלבד ולא מושפע מפונקציות אחרות. כלומר, הערך של המאפיין לא משתנה. לדוגמה, המאפיין 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) }
    }

שליחת כמה פקודות בבת אחת

ה-API של קיבוץ באצווה מאפשר ללקוח לשלוח כמה פקודות למכשיר Home APIs במטען ייעודי (payload) אחד. הפקודות נארזות במטען ייעודי (payload) יחיד ומופעלות במקביל, בדומה לאופן שבו אפשר ליצור אוטומציה של Home API באמצעות צומת מקביל, כמו בדוגמה פתיחת התריסים לפני הזריחה. עם זאת, Batching API מאפשר התנהגויות מורכבות ומתוחכמות יותר מאשר Automation API, כמו היכולת לבחור מכשירים באופן דינמי בזמן ריצה לפי קריטריונים כלשהם.

הפקודות באצווה אחת יכולות להיות מיועדות לכמה מאפיינים בכמה מכשירים, בכמה חדרים ובכמה מבנים.

שליחת פקודות באצווה מאפשרת למכשירים לבצע פעולות בו-זמנית, מה שלא אפשרי כששולחים פקודות ברצף בבקשות נפרדות. ההתנהגות שמתקבלת באמצעות פקודות באצ' מאפשרת למפתח להגדיר את המצב של קבוצת מכשירים כך שיתאים למצב מצטבר שנקבע מראש.

שימוש ב-Batching API

יש שלושה שלבים בסיסיים להפעלת פקודות באמצעות Batching API:

  1. מפעילים את השיטה Home.sendBatchedCommands().
  2. בתוך גוף הבלוק sendBatchedCommands(), מציינים את הפקודות שרוצים לכלול באצווה.
  3. בודקים את התוצאות של הפקודות שנשלחו כדי לראות אם הן הצליחו או נכשלו.

הפעלת השיטה sendBatchedCommands()

מבצעים קריאה ל-method‏ Home.sendBatchedCommands(). מאחורי הקלעים, השיטה הזו מגדירה ביטוי למבדה בהקשר מיוחד של אצווה.

home.sendBatchedCommands() {

ציון פקודות להפעלה על כמה משתמשים

בתוך גוף הבלוק sendBatchedCommands(), מאכלסים את batchable commands. פקודות שאפשר להריץ באצווה הן גרסאות 'צל' של פקודות קיימות ב-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()
}