יצירת פעולות אוטומטיות באמצעות ממשקי ה-API של Home ב-Android

1. לפני שמתחילים

זוהי סדנת הקוד השנייה בסדרה בנושא פיתוח אפליקציית Android באמצעות Google Home APIs. ב-codelab הזה נסביר איך ליצור אוטומציות לבית ונספק כמה טיפים לגבי שיטות מומלצות לשימוש בממשקי ה-API. אם עדיין לא סיימתם את ה-codelab הראשון, Build a mobile app using the Home APIs on Android, מומלץ לסיים אותו לפני שמתחילים את ה-codelab הזה.

ממשקי ה-API של Google Home מספקים קבוצה של ספריות למפתחי Android, שמאפשרות לשלוט במכשירים לבית החכם בסביבה העסקית של Google Home. בעזרת ממשקי ה-API החדשים האלה, מפתחים יוכלו להגדיר אוטומציות לבית חכם שיאפשרו שליטה ביכולות המכשירים על סמך תנאים מוגדרים מראש. ‫Google מספקת גם Discovery API שמאפשר לכם לשלוח שאילתות למכשירים כדי לגלות אילו מאפיינים ופקודות הם תומכים בהם.

דרישות מוקדמות

מה תלמדו

  • איך יוצרים אוטומציה למכשירים לבית חכם באמצעות ממשקי ה-API של Home.
  • איך משתמשים בממשקי Discovery API כדי לבדוק את היכולות הנתמכות של המכשיר.
  • איך מיישמים את השיטות המומלצות כשמפתחים אפליקציות באמצעות ממשקי ה-API של Home.

2. הגדרת הפרויקט

בתרשים הבא מוצגת הארכיטקטורה של אפליקציית Home APIs:

ארכיטקטורה של ממשקי Home API באפליקציית Android

  • קוד האפליקציה: קוד הליבה שעליו עובדים המפתחים כדי לבנות את ממשק המשתמש של האפליקציה ואת הלוגיקה לאינטראקציה עם Home APIs SDK.
  • Home APIs SDK: ערכת ה-SDK של Home APIs ש-Google מספקת פועלת עם שירות Home APIs ב-GMSCore כדי לשלוט במכשירי בית חכם. מפתחים יוצרים אפליקציות שעובדות עם ממשקי ה-API של Home על ידי חבילת ממשקי ה-API של Home עם ערכת ה-SDK.
  • GMSCore ב-Android:‏ GMSCore, שנקרא גם Google Play Services, הוא פלטפורמה של Google שמספקת שירותי ליבה למערכת, ומאפשרת להפעיל תכונות חשובות בכל מכשירי Android שאושרו. מודול הבית של Google Play Services מכיל את השירותים שמקיימים אינטראקציה עם ממשקי ה-API של Home.

ב-codelab הזה נמשיך את מה שהתחלנו ב-Build a mobile app using the Home APIs on Android.

מוודאים שיש בחשבון מבנה עם לפחות שני מכשירים נתמכים שמוגדרים ופועלים. ב-codelab הזה אנחנו הולכים להגדיר אוטומציות (שינוי במצב של מכשיר אחד מפעיל פעולה במכשיר אחר), ולכן תצטרכו שני מכשירים כדי לראות את התוצאות.

הורדת אפליקציה לדוגמה

קוד המקור של האפליקציה לדוגמה זמין ב-GitHub במאגר google-home/google-home-api-sample-app-android.

ב-codelab הזה נעשה שימוש בדוגמאות מהענף codelab-branch-2 של האפליקציה לדוגמה.

עוברים למיקום שבו רוצים לשמור את הפרויקט ומשכפלים את הענף codelab-branch-2:

$ git clone -b codelab-branch-2 https://github.com/google-home/google-home-api-sample-app-android.git

שימו לב: זה ענף אחר מזה שמשמש במאמר יצירת אפליקציה לנייד באמצעות ממשקי Home API ב-Android. ההסתעפות הזו של בסיס הקוד מתבססת על המקום שבו הסתיים ה-Codelab הראשון. הפעם, הדוגמאות מראות איך ליצור אוטומציות. אם השלמתם את ה-codelab הקודם והצלחתם להפעיל את כל הפונקציות, אתם יכולים להשתמש באותו פרויקט Android Studio כדי להשלים את ה-codelab הזה במקום להשתמש ב-codelab-branch-2.

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

3. מידע על אוטומציות

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

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

  1. התחלה – מגדיר את התנאים הראשוניים שמפעילים את האוטומציה, כמו שינוי בערך של מאפיין. לכל אוטומציה צריך להיות Starter.
  2. תנאי – אילוצים נוספים שצריך להעריך אחרי הפעלת האוטומציה. הערך של הביטוי בתנאי צריך להיות True כדי שהפעולות של האוטומציה יתבצעו.
  3. פעולה – פקודות או עדכוני סטטוס שמתבצעים כשכל התנאים מתקיימים.

לדוגמה, אפשר ליצור אוטומציה שמחלישה את האורות בחדר כשמעבירים מתג למצב אחר, בזמן שהטלוויזיה בחדר פועלת. בדוגמה הזו:

  • Starter – המתג בחדר מופעל.
  • תנאי – המצב של הטלוויזיה OnOff הוא On.
  • פעולה – עוצמת ההארה של הנורות באותו חדר שבו נמצא המתג תעומעם.

מנוע האוטומציה מעריך את הצמתים האלה באופן סדרתי או מקבילי.

image5.png

זרימה עוקבת מכילה צמתים שמופעלים בסדר עוקב. בדרך כלל אלה יהיו starter,‏ condition ו-action.

image6.png

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

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

image13.png

מפתחים מספקים את הצמתים האלה למנוע האוטומציה באמצעות שפה ספציפית לדומיין (DSL) שנוצרה במיוחד לאוטומציות של Google Home.

היכרות עם שפת התחום הספציפית (DSL) של האוטומציה

שפה ספציפית לדומיין (DSL) היא שפה שמשמשת לתיעוד של התנהגות המערכת בקוד. הקומפיילר יוצר מחלקות נתונים שעוברות סריאליזציה ל-JSON של מאגר פרוטוקולים, ומשמשות לביצוע קריאות לשירותי האוטומציה של Google.

שפת ה-DSL מחפשת את הסכימה הבאה:

automation {
name = "AutomationName"
  description = "An example automation description."
  isActive = true
    sequential {
    val onOffTrait = starter<_>(device1, OnOffLightDevice, OnOff)
    condition() { expression = onOffTrait.onOff equals true }
    action(device2, OnOffLightDevice) { command(OnOff.on()) }
  }
}

האוטומציה בדוגמה הקודמת מסנכרנת בין שתי נורות. כשהמצב OnOff של device1 משתנה ל-On (onOffTrait.onOff equals true), המצב OnOff של device2 משתנה ל-On (command(OnOff.on()).

כשעובדים עם אוטומציות, חשוב לדעת שיש מגבלות על משאבים.

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

4. יצירת כלי לעריכת אוטומציה

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

img11-01.png img11-02.png img11-03.png img11-04.png

הגדרת סימנים לתחילת פעולה

הסימן לתחילת פעולה אוטומטית הוא נקודת הכניסה לאוטומציה. התחלה מפעילה אוטומציה כשמתרחש אירוע מסוים. באפליקציית הדוגמה, אנחנו מתעדים את התחלות האוטומציה באמצעות המחלקה StarterViewModel, שנמצאת בקובץ המקור StarterViewModel.kt, ומציגים את תצוגת העריכה באמצעות StarterView (StarterView.kt).

צומת התחלתי צריך לכלול את הרכיבים הבאים:

  • מכשיר
  • תכונה
  • פעולה
  • ערך

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

האפליקציה מגדירה רשימה מוגדרת מראש של פעולות:

   // List of operations available when creating automation starters:
enum class Operation {
  EQUALS,
  NOT_EQUALS,
  GREATER_THAN,
  GREATER_THAN_OR_EQUALS,
  LESS_THAN,
  LESS_THAN_OR_EQUALS
    }

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

// List of operations available when comparing booleans:
 object BooleanOperations : Operations(listOf(
     Operation.EQUALS,
     Operation.NOT_EQUALS
 ))
// List of operations available when comparing values:
object LevelOperations : Operations(listOf(
    Operation.GREATER_THAN,
    Operation.GREATER_THAN_OR_EQUALS,
    Operation.LESS_THAN,
    Operation.LESS_THAN_OR_EQUALS
))

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

enum class OnOffValue {
   On,
   Off,
}
enum class ThermostatValue {
  Heat,
  Cool,
  Off,
}

הוא גם עוקב אחרי מיפוי בין הערכים שמוגדרים על ידי האפליקציה לבין הערכים שמוגדרים על ידי ממשקי ה-API:

val valuesOnOff: Map<OnOffValue, Boolean> = mapOf(
  OnOffValue.On to true,
  OnOffValue.Off to false,
)
val valuesThermostat: Map<ThermostatValue, ThermostatTrait.SystemModeEnum> = mapOf(
  ThermostatValue.Heat to ThermostatTrait.SystemModeEnum.Heat,
  ThermostatValue.Cool to ThermostatTrait.SystemModeEnum.Cool,
  ThermostatValue.Off to ThermostatTrait.SystemModeEnum.Off,
)

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

מסירים את ההערה משלב 4.1.1 בקובץ StarterView.kt כדי לעבד את כל מכשירי המתחילים ולהטמיע קריאה חוזרת (callback) בלחיצה ב-DropdownMenu:

val deviceVMs: List<DeviceViewModel> = structureVM.deviceVMs.collectAsState().value
...
DropdownMenu(expanded = expandedDeviceSelection, onDismissRequest = { expandedDeviceSelection = false }) {
// TODO: 4.1.1 - Starter device selection dropdown
// for (deviceVM in deviceVMs) {
//     DropdownMenuItem(
//         text = { Text(deviceVM.name) },
//         onClick = {
//             scope.launch {
//                 starterDeviceVM.value = deviceVM
//                 starterType.value = deviceVM.type.value
//                 starterTrait.value = null
//                 starterOperation.value = null
//             }
//             expandedDeviceSelection = false
//         }
//     )
// }
}

מסירים את ההערה מהשלב 4.1.2 בקובץ StarterView.kt כדי לעבד את כל המאפיינים של מכשיר המתחיל ולהטמיע קריאה חוזרת (callback) ללחיצה ב-DropdownMenu:

// Selected starter attributes for StarterView on screen:
val starterDeviceVM: MutableState<DeviceViewModel?> = remember {
mutableStateOf(starterVM.deviceVM.value) }
...
DropdownMenu(expanded = expandedTraitSelection, onDismissRequest = { expandedTraitSelection = false }) {
// TODO: 4.1.2 - Starter device traits selection dropdown
// val deviceTraits = starterDeviceVM.value?.traits?.collectAsState()?.value!!
// for (trait in deviceTraits) {
//     DropdownMenuItem(
//         text = { Text(trait.factory.toString()) },
//         onClick = {
//             scope.launch {
//                 starterTrait.value = trait.factory
//                 starterOperation.value = null
//             }
//             expandedTraitSelection = false
//         }
//     )
}
}

מבטלים את ההערה בשלב 4.1.3 בקובץ StarterView.kt כדי להציג את כל הפעולות של המאפיין שנבחר ולהטמיע קריאה חוזרת (callback) של קליקים ב-DropdownMenu:

val starterOperation: MutableState<StarterViewModel.Operation?> = remember {
  mutableStateOf(starterVM.operation.value) }
  ...
  DropdownMenu(expanded = expandedOperationSelection, onDismissRequest = { expandedOperationSelection = false }) {
    // ...
    if (!StarterViewModel.starterOperations.containsKey(starterTrait.value))
    return@DropdownMenu
    // TODO: 4.1.3 - Starter device trait operations selection dropdown
      // val operations: List<StarterViewModel.Operation> = StarterViewModel.starterOperations.get(starterTrait.value ?: OnOff)?.operations!!
    //  for (operation in operations) {
    //      DropdownMenuItem(
    //          text = { Text(operation.toString()) },
    //          onClick = {
    //              scope.launch {
    //                  starterOperation.value = operation
    //              }
    //              expandedOperationSelection = false
    //          }
    //      )
    //  }
}

מסירים את ההערה משלב 4.1.4 בקובץ StarterView.kt כדי להציג את כל הערכים של התכונה שנבחרה ומטמיעים קריאה חוזרת (callback) בלחיצה ב-DropdownMenu:

when (starterTrait.value) {
  OnOff -> {
        ...
    DropdownMenu(expanded = expandedBooleanSelection, onDismissRequest = { expandedBooleanSelection = false }) {
// TODO: 4.1.4 - Starter device trait values selection dropdown
//             for (value in StarterViewModel.valuesOnOff.keys) {
//                 DropdownMenuItem(
//                     text = { Text(value.toString()) },
//                     onClick = {
//                         scope.launch {
//                             starterValueOnOff.value = StarterViewModel.valuesOnOff.get(value)
//                         }
//                         expandedBooleanSelection = false
//                     }
//                 )
//             }
             }
              ...
          }
           LevelControl -> {
              ...
      }
   }

מבטלים את ההערה בשלב 4.1.5 בקובץ StarterView.kt כדי לאחסן את כל משתני ההפעלה ViewModel בהפעלה ViewModel של הטיוטה (draftVM.starterVMs).

val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save starter button:
Button(
enabled = isOptionsSelected && isValueProvided,
onClick = {
  scope.launch {
  // TODO: 4.1.5 - store all starter ViewModel variables into draft ViewModel
  // starterVM.deviceVM.emit(starterDeviceVM.value)
  // starterVM.trait.emit(starterTrait.value)
  // starterVM.operation.emit(starterOperation.value)
  // starterVM.valueOnOff.emit(starterValueOnOff.value!!)
  // starterVM.valueLevel.emit(starterValueLevel.value!!)
  // starterVM.valueBooleanState.emit(starterValueBooleanState.value!!)
  // starterVM.valueOccupancy.emit(starterValueOccupancy.value!!)
  // starterVM.valueThermostat.emit(starterValueThermostat.value!!)
  //
  // draftVM.starterVMs.value.add(starterVM)
  // draftVM.selectedStarterVM.emit(null)
  }
})
{ Text(stringResource(R.string.starter_button_create)) }

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

79beb3b581ec71ec.png

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

הגדרת פעולות

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

אפליקציית הדוגמה משתמשת בישויות הבאות של Home APIs כדי להגדיר את צמתי הפעולה של האוטומציה:

  • מכשיר
  • תכונה
  • פקודה
  • ערך (אופציונלי)

כל פעולת פקודה למכשיר משתמשת בפקודה, אבל חלק מהפעולות ידרשו גם ערך פרמטר שמשויך אליהן, כמו MoveToLevel() ואחוז יעד.

אפשר לבחור את המכשיר והמאפיין מתוך האובייקטים שמוחזרים על ידי Devices API.

האפליקציה מגדירה רשימה מוגדרת מראש של פקודות:

   // List of operations available when creating automation starters:
enum class Action {
  ON,
  OFF,
  MOVE_TO_LEVEL,
  MODE_HEAT,
  MODE_COOL,
  MODE_OFF,
}

האפליקציה עוקבת אחרי הפעולות הנתמכות לכל מאפיין נתמך:

 // List of operations available when comparing booleans:
object OnOffActions : Actions(listOf(
    Action.ON,
    Action.OFF,
))
// List of operations available when comparing booleans:
object LevelActions : Actions(listOf(
    Action.MOVE_TO_LEVEL
))
// List of operations available when comparing booleans:
object ThermostatActions : Actions(listOf(
    Action.MODE_HEAT,
    Action.MODE_COOL,
    Action.MODE_OFF,
))
// Map traits and the comparison operations they support:
val actionActions: Map<TraitFactory<out Trait>, Actions> = mapOf(
    OnOff to OnOffActions,
    LevelControl to LevelActions,
 // BooleanState - No Actions
 // OccupancySensing - No Actions
    Thermostat to ThermostatActions,
)

לפקודות עם פרמטר אחד או יותר יש גם משתנה:

   val valueLevel: MutableStateFlow<UByte?>

ה-API מציג קבוצה של רכיבי תצוגה שמשתמשים יכולים להשתמש בהם כדי לבחור את השדות הנדרשים.

מבטלים את ההערה בשלב 4.2.1 בקובץ ActionView.kt כדי לעבד את כל מכשירי הפעולה וליישם קריאה חוזרת (callback) של קליקים ב-DropdownMenu כדי להגדיר את actionDeviceVM.

val deviceVMs = structureVM.deviceVMs.collectAsState().value
...
DropdownMenu(expanded = expandedDeviceSelection, onDismissRequest = { expandedDeviceSelection = false }) {
// TODO: 4.2.1 - Action device selection dropdown
// for (deviceVM in deviceVMs) {
//     DropdownMenuItem(
//         text = { Text(deviceVM.name) },
//         onClick = {
//             scope.launch {
//                 actionDeviceVM.value = deviceVM
//                 actionTrait.value = null
//                 actionAction.value = null
//             }
//             expandedDeviceSelection = false
//         }
//     )
// }
}

מבטלים את ההערה בשלב 4.2.2 בקובץ ActionView.kt כדי לעבד את כל המאפיינים של actionDeviceVM ומטמיעים קריאה חוזרת (callback) של קליקים ב-DropdownMenu כדי להגדיר את actionTrait, שמייצג את המאפיין שאליו הפקודה שייכת.

val actionDeviceVM: MutableState<DeviceViewModel?> = remember {
mutableStateOf(actionVM.deviceVM.value) }
...
DropdownMenu(expanded = expandedTraitSelection, onDismissRequest = { expandedTraitSelection = false }) {
// TODO: 4.2.2 - Action device traits selection dropdown
// val deviceTraits: List<Trait> = actionDeviceVM.value?.traits?.collectAsState()?.value!!
// for (trait in deviceTraits) {
//     DropdownMenuItem(
//         text = { Text(trait.factory.toString()) },
//         onClick = {
//             scope.launch {
//                 actionTrait.value = trait
//                 actionAction.value = null
//             }
//             expandedTraitSelection = false
//         }
//     )
// }
}

מסירים את ההערה משלב 4.2.3 בקובץ ActionView.kt כדי להציג את כל הפעולות הזמינות של actionTrait ומטמיעים קריאה חוזרת (callback) של קליקים ב-DropdownMenu כדי להגדיר את actionAction, שמייצג את פעולת האוטומציה שנבחרה.

DropdownMenu(expanded = expandedActionSelection, onDismissRequest = { expandedActionSelection = false }) {
// ...
if (!ActionViewModel.actionActions.containsKey(actionTrait.value?.factory))
return@DropdownMenu
// TODO: 4.2.3 - Action device trait actions (commands) selection dropdown
// val actions: List<ActionViewModel.Action> = ActionViewModel.actionActions.get(actionTrait.value?.factory)?.actions!!
// for (action in actions) {
//     DropdownMenuItem(
//         text = { Text(action.toString()) },
//         onClick = {
//             scope.launch {
//                 actionAction.value = action
//             }
//             expandedActionSelection = false
//         }
//     )
// }
}

מבטלים את ההערה בשלב 4.2.4 בקובץ ActionView.kt כדי להציג את הערכים הזמינים של פעולת התכונה (הפקודה) ולאחסן את הערך ב-actionValueLevel בקריאה החוזרת לשינוי הערך:

when (actionTrait.value?.factory) {
LevelControl -> {
// TODO: 4.2.4 - Action device trait action(command) values selection widget
// Column (Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth()) {
//   Text(stringResource(R.string.action_title_value), fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
//  }
//
//  Box (Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
//      LevelSlider(value = actionValueLevel.value?.toFloat()!!, low = 0f, high = 254f, steps = 0,
//          modifier = Modifier.padding(top = 16.dp),
//          onValueChange = { value : Float -> actionValueLevel.value = value.toUInt().toUByte() }
//          isEnabled = true
//      )
//  }
...
}

מבטלים את ההערה בשלב 4.2.5 בקובץ ActionView.kt כדי לאחסן את כל המשתנים של פעולה ViewModel בפעולה ViewModel של טיוטת האוטומציה (draftVM.actionVMs):

val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save action button:
Button(
  enabled = isOptionsSelected,
  onClick = {
  scope.launch {
  // TODO: 4.2.5 - store all action ViewModel variables into draft ViewModel
  // actionVM.deviceVM.emit(actionDeviceVM.value)
  // actionVM.trait.emit(actionTrait.value)
  // actionVM.action.emit(actionAction.value)
  // actionVM.valueLevel.emit(actionValueLevel.value)
  //
  // draftVM.actionVMs.value.add(actionVM)
  // draftVM.selectedActionVM.emit(null)
  }
})
{ Text(stringResource(R.string.action_button_create)) }

הפעלת האפליקציה ובחירה בפעולה אוטומטית ובפעולה חדשה אמורות להוביל לתצוגה כמו זו שבהמשך:

6efa3c7cafd3e595.png

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

עיבוד של טיוטה של תרחיש אוטומטי

כשDraftViewModel מסתיים, אפשר לעבד אותו באמצעות HomeAppView.kt:

fun HomeAppView (homeAppVM: HomeAppViewModel) {
  ...
  // If a draft automation is selected, show the draft editor:
  if (selectedDraftVM != null) {
    DraftView(homeAppVM)
  }
  ...
}

בDraftView.kt:

fun DraftView (homeAppVM: HomeAppViewModel) {
   val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
    ...
// Draft Starters:
   DraftStarterList(draftVM)
// Draft Actions:
   DraftActionList(draftVM)
}

יצירת פעולה אוטומטית

עכשיו, אחרי שלמדתם איך ליצור התחלות ופעולות, אתם מוכנים ליצור טיוטה של אוטומציה ולשלוח אותה אל Automation API. ל-API יש פונקציה createAutomation() שמקבלת טיוטה של אוטומציה כארגומנט ומחזירה מופע חדש של אוטומציה.

ההכנה של טיוטת האוטומציה מתבצעת במחלקה DraftViewModel באפליקציית הדוגמה. כדאי לעיין בפונקציה getDraftAutomation() כדי לקבל מידע נוסף על האופן שבו אנחנו יוצרים את טיוטת האוטומציה באמצעות משתני ההתחלה והפעולה שמופיעים בקטע הקודם.

מבטלים את ההערה בשלב 4.4.1 בקובץ DraftViewModel.kt כדי ליצור את הביטויים של select שנדרשים ליצירת גרף האוטומציה כשמאפיין ההתחלה הוא OnOff:

val starterVMs: List<StarterViewModel> = starterVMs.value
val actionVMs: List<ActionViewModel> = actionVMs.value
    ...
fun getDraftAutomation() : DraftAutomation {
    ...
  val starterVMs: List<StarterViewModel> = starterVMs.value
    ...
  return automation {
    this.name = name
    this.description = description
    this.isActive = true
    // The sequential block wrapping all nodes:
    sequential {
    // The select block wrapping all starters:
      select {
    // Iterate through the selected starters:
        for (starterVM in starterVMs) {
        // The sequential block for each starter (should wrap the Starter Expression!)
          sequential {
              ...
              val starterTrait: TraitFactory<out Trait> = starterVM.trait.value!!
              ...
              when (starterTrait) {
                  OnOff -> {
        // TODO: 4.4.1 - Set starter expressions according to trait type
        //   val onOffValue: Boolean = starterVM.valueOnOff.value
        //   val onOffExpression: TypedExpression<out OnOff> =
        //       starterExpression as TypedExpression<out OnOff>
        //   when (starterOperation) {
        //       StarterViewModel.Operation.EQUALS ->
        //           condition { expression = onOffExpression.onOff equals onOffValue }
        //       StarterViewModel.Operation.NOT_EQUALS ->
        //           condition { expression = onOffExpression.onOff notEquals onOffValue }
        //       else -> { MainActivity.showError(this, "Unexpected operation for OnOf
        //   }
        }
   LevelControl -> {
     ...
// Function to allow manual execution of the automation:
manualStarter()
     ...
}

מבטלים את ההערה בשלב 4.4.2 בקובץ DraftViewModel.kt כדי ליצור את הביטויים המקבילים שנדרשים ליצירת גרף האוטומציה כשמאפיין הפעולה שנבחר הוא LevelControl והפעולה שנבחרה היא MOVE_TO_LEVEL:

val starterVMs: List<StarterViewModel> = starterVMs.value
val actionVMs: List<ActionViewModel> = actionVMs.value
    ...
fun getDraftAutomation() : DraftAutomation {
      ...
  return automation {
    this.name = name
    this.description = description
    this.isActive = true
    // The sequential block wrapping all nodes:
    sequential {
          ...
    // Parallel block wrapping all actions:
      parallel {
        // Iterate through the selected actions:
        for (actionVM in actionVMs) {
          val actionDeviceVM: DeviceViewModel = actionVM.deviceVM.value!!
        // Action Expression that the DSL will check for:
          action(actionDeviceVM.device, actionDeviceVM.type.value.factory) {
            val actionCommand: Command = when (actionVM.action.value) {
                  ActionViewModel.Action.ON -> { OnOff.on() }
                  ActionViewModel.Action.OFF -> { OnOff.off() }
    // TODO: 4.4.2 - Set starter expressions according to trait type
    // ActionViewModel.Action.MOVE_TO_LEVEL -> {
    //     LevelControl.moveToLevelWithOnOff(
    //         actionVM.valueLevel.value!!,
    //         0u,
    //         LevelControlTrait.OptionsBitmap(),
    //         LevelControlTrait.OptionsBitmap()
    //     )
    // }
      ActionViewModel.Action.MODE_HEAT -> { SimplifiedThermostat
      .setSystemMode(SimplifiedThermostatTrait.SystemModeEnum.Heat) }
          ...
}

השלב האחרון בהשלמת האוטומציה הוא הטמעה של הפונקציה getDraftAutomation כדי ליצור AutomationDraft.

מבטלים את ההערה בשלב 4.4.3 בקובץ HomeAppViewModel.kt כדי ליצור את האוטומציה על ידי קריאה לממשקי ה-API של Home וטיפול בחריגים:

fun createAutomation(isPending: MutableState<Boolean>) {
  viewModelScope.launch {
    val structure : Structure = selectedStructureVM.value?.structure!!
    val draft : DraftAutomation = selectedDraftVM.value?.getDraftAutomation()!!
    isPending.value = true
    // TODO: 4.4.3 - Call the Home API to create automation and handle exceptions
    // // Call Automation API to create an automation from a draft:
    // try {
    //     structure.createAutomation(draft)
    // }
    // catch (e: Exception) {
    //     MainActivity.showError(this, e.toString())
    //     isPending.value = false
    //     return@launch
    // }
    // Scrap the draft and automation candidates used in the process:
    selectedCandidateVMs.emit(null)
    selectedDraftVM.emit(null)
    isPending.value = false
  }
}

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

אחרי שבוחרים את הסימן לתחילת פעולה ואת הפעולה, אפשר ליצור את האוטומציה:

ec551405f8b07b8e.png

חשוב לתת לאוטומציה שם ייחודי, ואז לוחצים על הלחצן יצירת אוטומציה. הפעולה הזו אמורה להפעיל את ממשקי ה-API ולהחזיר אתכם לתצוגת הרשימה של האוטומציות עם האוטומציה שיצרתם:

8eebc32cd3755618.png

מקישים על האוטומציה שיצרתם ורואים איך היא מוחזרת על ידי ממשקי ה-API.

931dba7c325d6ef7.png

חשוב לדעת שה-API מחזיר ערך שמציין אם אמצעי אוטומטי תקף ופעיל כרגע. אפשר ליצור אוטומציות שלא עוברות אימות כשהן מפוענחות בצד השרת. אם ניתוח של אוטומציה נכשל באימות, הערך של isValid מוגדר ל-false, מה שמציין שהאוטומציה לא תקינה ולא פעילה. אם האוטומציה לא תקינה, אפשר לבדוק את השדה automation.validationIssues כדי לקבל פרטים.

מוודאים שהאוטומציה מוגדרת כפעילה ותקפה, ואז מנסים להפעיל אותה.

התנסות בפעולה האוטומטית

אפשר להפעיל אוטומציות בשתי דרכים:

  1. עם אירוע של סימן לתחילת פעולה. אם התנאים מתקיימים, הפעולה שהגדרתם באוטומציה מופעלת.
  2. באמצעות קריאה ל-API של הפעלה ידנית.

אם מוגדר manualStarter() בבלוק ה-DSL של טיוטת האוטומציה, מנוע האוטומציה יתמוך בהפעלה ידנית של האוטומציה הזו. הוא כבר מופיע בדוגמאות הקוד באפליקציה לדוגמה.

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

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

עוברים לכרטיסייה Devices (מכשירים), בוחרים את מכשיר הפעולה ואת המאפיין, ומגדירים ערך אחר (לדוגמה, מגדירים את light2's LevelControl (בהירות) ל-50%, כמו שמודגם בצילום המסך הבא:

d0357ec71325d1a8.png

עכשיו ננסה להפעיל את האוטומציה באמצעות המכשיר שמשמש כמרכז הבקרה. בוחרים את המכשיר שמשמש כסימן לתחילת הפעולה שבחרתם כשיצרתם את האוטומציה. מעבירים את המתג של המאפיין שבחרתם (לדוגמה, מגדירים את starter outlet1OnOff לOn):

230c78cd71c95564.png

אפשר לראות שהפעולה הזו מפעילה גם את האוטומציה ומגדירה את מאפיין LevelControl של מכשיר הפעולה light2 לערך המקורי, 100%:

1f00292128bde1c2.png

הצלחתם להשתמש בממשקי ה-API של Home כדי ליצור אוטומציות.

מידע נוסף על Automation API זמין במאמר Android Automation API.

5. גילוי יכולות

ממשקי ה-API של Home כוללים API ייעודי בשם Discovery API, שמפתחים יכולים להשתמש בו כדי לשאול אילו תכונות עם יכולות אוטומציה נתמכות במכשיר נתון. באפליקציית הדוגמה יש דוגמה לשימוש ב-API הזה כדי לגלות אילו פקודות זמינות.

הסבר על פקודות

בקטע הזה נסביר איך לגלות CommandCandidates נתמכים ואיך ליצור אוטומציה על סמך צמתי מועמדים שזוהו.

באפליקציה לדוגמה, אנחנו קוראים ל-device.candidates() כדי לקבל רשימה של מועמדים, שעשויה לכלול מקרים של CommandCandidate, ‏ EventCandidate או TraitAttributesCandidate.

עוברים לקובץ HomeAppViewModel.kt ומבטלים את ההערה בשלב 5.1.1 כדי לאחזר את רשימת התווים האפשריים ולסנן לפי סוג Candidate:

   fun showCandidates() {

   ...
// TODO: 5.1.1 - Retrieve automation candidates, filtering to include CommandCandidate types only
// // Retrieve a set of initial automation candidates from the device:
// val candidates: Set<NodeCandidate> = deviceVM.device.candidates().first()
//
// for (candidate in candidates) {
//     // Check whether the candidate trait is supported:
//     if(candidate.trait !in HomeApp.supportedTraits)
//         continue
//     // Check whether the candidate type is supported:
//     when (candidate) {
//         // Command candidate type:
//         is CommandCandidate -> {
//             // Check whether the command candidate has a supported command:
//             if (candidate.commandDescriptor !in ActionViewModel.commandMap)
//                 continue
//         }
//         // Other candidate types are currently unsupported:
//         else -> { continue }
//     }
//
//     candidateVMList.add(CandidateViewModel(candidate, deviceVM))
// }
...
           // Store the ViewModels:
selectedCandidateVMs.emit(candidateVMList)
}

אפשר לראות איך המסנן פועל לגבי CommandCandidate. המועמדים שמוחזרים על ידי ה-API שייכים לסוגים שונים. אפליקציית הדוגמה תומכת ב-CommandCandidate. מבטלים את ההערה בשלב 5.1.2 ב-commandMap שמוגדר ב-ActionViewModel.kt כדי להגדיר את המאפיינים הנתמכים האלה:

    // Map of supported commands from Discovery API:
val commandMap: Map<CommandDescriptor, Action> = mapOf(
    // TODO: 5.1.2 - Set current supported commands
    // OnOffTrait.OnCommand to Action.ON,
    // OnOffTrait.OffCommand to Action.OFF,
    // LevelControlTrait.MoveToLevelWithOnOffCommand to Action.MOVE_TO_LEVEL
)

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

8a2f0e8940f7056a.png

מידע נוסף על Discovery API זמין במאמר שימוש באיתור מכשירים ב-Android.

שילוב העורך

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

הקובץ CandidatesView.kt מכיל את מחלקת התצוגה שמציגה את המועמדים שנמצאו. מבטלים את ההערה בשלב 5.2.1 כדי להפעיל את הפונקציה CandidateListItem של .clickable{}, שמגדירה את homeAppVM.selectedDraftVM כ-candidateVM:

fun CandidateListItem (candidateVM: CandidateViewModel, homeAppVM: HomeAppViewModel) {
    val scope: CoroutineScope = rememberCoroutineScope()
    Box (Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
        Column (Modifier.fillMaxWidth().clickable {
        // TODO: 5.2.1 - Set the selectedDraftVM to the selected candidate
        // scope.launch { homeAppVM.selectedDraftVM.emit(DraftViewModel(candidateVM)) }
        }) {
            ...
        }
    }
}

בדומה לשלב 4.3 ב-HomeAppView.kt, כשמגדירים את selectedDraftVM, המערכת מעבדת את DraftView(...) in DraftView.kt`:

fun HomeAppView (homeAppVM: HomeAppViewModel) {
   ...
  val selectedDraftVM: DraftViewModel? by homeAppVM.selectedDraftVM.collectAsState()
...
  // If a draft automation is selected, show the draft editor:
  if (selectedDraftVM != null) {
  DraftView(homeAppVM)
  }
   ...
}

אפשר לנסות שוב על ידי הקשה על light2 - MOVE_TO_LEVEL, שמוצג בקטע הקודם, ומופיעה בקשה ליצור אוטומציה חדשה על סמך הפקודה של המועמד:

15e67763a9241000.png

אחרי שהכרתם את תהליך יצירת האוטומציה באפליקציה לדוגמה, אתם יכולים לשלב אוטומציות באפליקציות שלכם.

6. דוגמאות לאוטומציה מתקדמת

לפני שנסיים, נדון בכמה דוגמאות נוספות של DSL לאוטומציה. הדוגמאות האלה ממחישות חלק מהיכולות המתקדמות שאפשר להשיג באמצעות ממשקי ה-API.

התחלה של יום

בנוסף למאפייני המכשיר, ממשקי Google Home API מציעים מאפיינים מבוססי-מבנה, כמו Time. אתם יכולים ליצור פעולה אוטומטית עם התנעה מבוססת-זמן, כמו הפעולה הבאה:

automation {
  name = "AutomationName"
  description = "An example automation description."
  isActive = true
  description = "Do ... actions when time is up."
  sequential {
    // starter
    val starter = starter<_>(structure, Time.ScheduledTimeEvent) {
      parameter(
        Time.ScheduledTimeEvent.clockTime(
          LocalTime.of(hour, min, sec, 0)
        )
      )
    }
        // action
  ...
  }
}

שידור ב-Assistant כפעולה

התכונה AssistantBroadcast זמינה כתכונה ברמת המכשיר ב-SpeakerDevice (אם הרמקול תומך בה) או כתכונה ברמת המבנה (כי רמקולים של Google ומכשירים ניידים עם Android יכולים להפעיל שידורים של Assistant). לדוגמה:

automation {
  name = "AutomationName"
  description = "An example automation description."
  isActive = true
  description = "Broadcast in Speaker when ..."
  sequential {
    // starter
      ...
    // action
    action(structure) {
      command(
      AssistantBroadcast.broadcast("Time is up!!")
      )
    }
  }
}

שימוש ב-DelayFor וב-suppressFor

בנוסף, Automation API מספק אופרטורים מתקדמים כמו delayFor, שמשמש להשהיית פקודות, ו-suppressFor, שיכול למנוע הפעלה של אוטומציה על ידי אותם אירועים בטווח זמן נתון. הנה כמה דוגמאות לשימוש באופרטורים האלה:

sequential {
  val starterNode = starter<_>(device, OccupancySensorDevice, MotionDetection)
  // only proceed if there is currently motion taking place
  condition { starterNode.motionDetectionEventInProgress equals true }
   // ignore the starter for one minute after it was last triggered
    suppressFor(Duration.ofMinutes(1))
  
    // make announcements three seconds apart
    action(device, SpeakerDevice) {
      command(AssistantBroadcast.broadcast("Intruder detected!"))
    }
    delayFor(Duration.ofSeconds(3))
    action(device, SpeakerDevice) {
    command(AssistantBroadcast.broadcast("Intruder detected!"))
  }
    ...
}

שימוש ב-AreaPresenceState בסימן לתחילת הפעולה

AreaPresenceState היא תכונה ברמת המבנה שמזהה אם יש מישהו בבית.

לדוגמה, בדוגמה הבאה מוצגת נעילה אוטומטית של הדלתות כשמישהו נמצא בבית אחרי 22:00:

automation {
  name = "Lock the doors when someone is home after 10pm"
  description = "1 starter, 2 actions"
  sequential {
    val unused =
      starter(structure, event = Time.ScheduledTimeEvent) {
        parameter(Time.ScheduledTimeEvent.clockTime(LocalTime.of(22, 0, 0, 0)))
      }
    val stateReaderNode = stateReader<_>(structure, AreaPresenceState)
    condition {
      expression =
        stateReaderNode.presenceState equals
          AreaPresenceStateTrait.PresenceState.PresenceStateOccupied
    }
    action(structure) { command(AssistantBroadcast.broadcast("Locks are being applied")) }
    for (lockDevice in lockDevices) {
      action(lockDevice, DoorLockDevice) {
        command(Command(DoorLock, DoorLockTrait.LockDoorCommand.requestId.toString(), mapOf()))
      }
    }
  }

עכשיו, אחרי שהכרתם את היכולות המתקדמות של האוטומציה, אתם יכולים לצאת וליצור אפליקציות מדהימות!

7. מעולה!

מעולה! סיימתם בהצלחה את החלק השני של פיתוח אפליקציית Android באמצעות Google Home APIs. במהלך ה-codelab הזה, למדתם על ממשקי ה-API של אוטומציה וגילוי.

אנחנו מקווים שתיהנו מפיתוח אפליקציות ששולטות במכשירים בצורה יצירתית בסביבת Google Home, ומיצירת תרחישי אוטומציה מעניינים באמצעות ממשקי ה-API של Home.

השלבים הבאים

  • במאמר פתרון בעיות מוסבר איך לנפות באגים באפליקציות ולפתור בעיות שקשורות לממשקי ה-API של Home.
  • אתם יכולים לשלוח לנו המלצות או לדווח על בעיות דרך מעקב הבעיות בנושא התמיכה בבית חכם.