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

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

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

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

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

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

מה תלמדו

  • איך יוצרים אוטומציה למכשירים לבית חכם באמצעות ממשקי ה-API של Home.
  • איך משתמשים ב-Discovery APIs כדי לבדוק את היכולות הנתמכות של המכשיר.
  • איך מיישמים את השיטות המומלצות כשמפתחים אפליקציות באמצעות ממשקי ה-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. סימן לתחילת פעולה – מגדיר את התנאים הראשוניים שמפעילים את הפעולות האוטומטיות, כמו שינוי בערך של מאפיין. לכל פעולה אוטומטית צריך להיות סימן לתחילת פעולה.
  2. תנאי – אילוצים נוספים שצריך להעריך אחרי שהופעל אוטומציה. הערך של הביטוי בתנאי צריך להיות True כדי שהפעולות של האוטומציה יתבצעו.
  3. פעולה – פקודות או עדכוני סטטוס שמתבצעים כשכל התנאים מתקיימים.

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

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

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

image5.png

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

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
//         }
//     )
// }
}

מבטלים את הסימון כהערה (uncomment) של שלב 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 API. במהלך ה-Codelab הזה, למדתם על ממשקי ה-API של אוטומציה וגילוי.

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

השלבים הבאים

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