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

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

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

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

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

מה תלמדו

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

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

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

הארכיטקטורה של Home APIs לאפליקציה ל-Android

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

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

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

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

קוד המקור של האפליקציה לדוגמה זמין ב-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

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

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

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

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

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

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

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

  • Starter – המתג בחדר מופעל.
  • Condition (תנאי) – המצב של מצב ההפעלה/השבתה של הטלוויזיה מקבל את הערך 'מופעל'.
  • פעולה – התאורה בחדר שבו נמצא המתג תהיה עמומה יותר.

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

image5.png

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

image6.png

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

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

image13.png

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

הסבר על Automation 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 ב-starter של האוטומציה של הטיוטה (draftVM.starterVMs).ViewModel

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

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

הגדרת פעולות

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

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, הגיע הזמן לבדוק אם היא פועלת גם באמצעות ה-starter שהגדרתם.

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

d0357ec71325d1a8.png

עכשיו ננסה להפעיל את האוטומציה באמצעות מכשיר ההתחלה. בוחרים את המכשיר להתחלת הפעולה שבחרתם כשיצרתם את האוטומציה. משנים את המאפיין שבחרתם (לדוגמה, מגדירים את OnOff של starter outlet1 כ-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 כדי להפעיל את הפונקציה .clickable{} של CandidateListItem שמגדירה את 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.

שעת ההתחלה כגורם מעורר

בנוסף למאפייני המכשיר, ממשקי ה-API של Google Home מציעים מאפיינים מבוססי-מבנה, כמו 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 ב-starter

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 באמצעות ממשקי ה-API של Google Home. במהלך הקודלאב הזה, הכרתם את Automation API ו-Discovery API.

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

השלבים הבאים

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