1. לפני שמתחילים
זהו ה-Codelab השני בסדרה בנושא פיתוח אפליקציית Android באמצעות ממשקי Google Home API. ב-codelab הזה נסביר איך ליצור אוטומציות לבית ונספק כמה טיפים לגבי שיטות מומלצות לשימוש בממשקי ה-API. אם עדיין לא סיימתם את ה-codelab הראשון, Build a mobile app using the Home APIs on Android, מומלץ לסיים אותו לפני שמתחילים את ה-codelab הזה.
ממשקי ה-API של Google Home מספקים קבוצה של ספריות למפתחי Android, שמאפשרות לשלוט במכשירים לבית החכם בסביבה העסקית של Google Home. בעזרת ממשקי ה-API החדשים האלה, מפתחים יוכלו להגדיר אוטומציות לבית חכם שיאפשרו לשלוט ביכולות של המכשירים על סמך תנאים מוגדרים מראש. Google מספקת גם Discovery API שמאפשר לכם לשלוח שאילתות למכשירים כדי לגלות אילו מאפיינים ופקודות הם תומכים בהם.
בסרטון הזה מוצגות דוגמאות קצרות לאוטומציות שתיצרו, לכן מומלץ לצפות בסרטון תוך כדי ביצוע ההוראות ב-codelab.
דרישות מוקדמות
- משלימים את ה-codelab Build a mobile app using the Home APIs on Android (פיתוח אפליקציה לנייד באמצעות Home APIs ב-Android).
- ידע במערכת האקולוגית של Google Home (Cloud-to-cloud ו-Matter).
- תחנת עבודה עם Android Studio (גרסה 2024.3.1 Ladybug ואילך) מותקנת.
- טלפון Android שעומד בדרישות של ממשקי ה-API של Home (ראו דרישות מוקדמות), עם Google Play Services ואפליקציית Google Home מותקנות. אי אפשר להשתמש באמולטור, יש תמיכה רק בטלפונים פיזיים עם Android עבור אפליקציית הדוגמה.
- Google Home Hub תואם שתומך ב-Google Home APIs.
- אופציונלי – מכשיר לבית חכם שתואם לממשקי Google Home API.
מה תלמדו
- איך יוצרים אוטומציה למכשירים לבית חכם באמצעות ממשקי ה-API של Home.
- איך משתמשים ב-Discovery APIs כדי לבדוק את היכולות הנתמכות של המכשיר.
- איך מיישמים את השיטות המומלצות כשמפתחים אפליקציות באמצעות ממשקי ה-API של Home.
2. הגדרת הפרויקט
הדיאגרמה הבאה ממחישה את הארכיטקטורה של אפליקציית Home APIs:

- קוד האפליקציה: קוד הליבה שעליו עובדים המפתחים כדי לבנות את ממשק המשתמש של האפליקציה ואת הלוגיקה לאינטראקציה עם Home APIs SDK.
- Home APIs SDK: ערכת ה-SDK של Home APIs ש-Google מספקת פועלת עם שירות Home APIs ב-GMSCore כדי לשלוט במכשירי בית חכם. מפתחים בונים אפליקציות שעובדות עם ממשקי ה-API של Home על ידי קיבוץ שלהן עם ערכת ה-SDK של ממשקי ה-API של Home.
- 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 שלהם.
אוטומציות מורכבות משלושה סוגים שונים של רכיבים שנקראים צמתים: התחלות, פעולות ותנאים. הצמתים האלה פועלים יחד כדי לבצע אוטומציה של התנהגויות באמצעות מכשירים לבית החכם. בדרך כלל, ההערכה מתבצעת לפי הסדר הבא:
- סימן לתחילת הפעולה – מגדיר את התנאים הראשוניים שמפעילים את האוטומציה, כמו שינוי בערך של מאפיין. לכל אוטומציה צריך להיות Starter.
- תנאי – אילוצים נוספים להערכה אחרי הפעלת האוטומציה. הערך של הביטוי בתנאי צריך להיות True כדי שהפעולות של האוטומציה יתבצעו.
- פעולה – פקודות או עדכוני סטטוס שמתבצעים כשכל התנאים מתקיימים.
לדוגמה, אפשר ליצור אוטומציה שמחלישה את האורות בחדר כשמעבירים מתג למצב אחר, בזמן שהטלוויזיה בחדר פועלת. בדוגמה הזו:
- Starter – המתג בחדר מופעל.
- תנאי – המצב של הטלוויזיה OnOff הוא On.
- פעולה – עוצמת התאורה של הנורות באותו חדר שבו נמצא המתג תופחת.
מנוע האוטומציה מעריך את הצמתים האלה באופן סדרתי או מקבילי.

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

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

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

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

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

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

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

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

חשוב לדעת שממשק ה-API מחזיר ערך שמציין אם האוטומציה תקפה ופעילה כרגע. אפשר ליצור אוטומציות שלא עוברות אימות כשהן מפוענחות בצד השרת. אם ניתוח של אוטומציה נכשל באימות, הערך של isValid מוגדר ל-false, מה שמציין שהאוטומציה לא תקינה ולא פעילה. אם האוטומציה לא תקינה, אפשר לבדוק את השדה automation.validationIssues כדי לקבל פרטים.
מוודאים שהאוטומציה מוגדרת כפעילה ותקפה, ואז מנסים להפעיל אותה.
התנסות בפעולה האוטומטית
אפשר להפעיל את האוטומציות בשתי דרכים:
- עם אירוע של סימן לתחילת פעולה. אם התנאים מתאימים, הפעולה שהגדרתם באוטומציה מופעלת.
- באמצעות קריאה ידנית ל-API.
אם מוגדר manualStarter() בבלוק ה-DSL של טיוטת האוטומציה, מנוע האוטומציה יתמוך בהפעלה ידנית של האוטומציה הזו. הוא כבר מופיע בדוגמאות הקוד באפליקציה לדוגמה.
מכיוון שאתם עדיין במסך תצוגת האוטומציה בנייד, מקישים על הלחצן הפעלה ידנית. הפקודה הזו אמורה להתקשר אל automation.execute(), שמריץ את פקודת הפעולה במכשיר שבחרתם כשקבעתם את ההגדרה של האוטומציה.
אחרי שמאמתים את פקודת הפעולה באמצעות הפעלה ידנית דרך ה-API, הגיע הזמן לבדוק אם היא מופעלת גם באמצעות סימן לתחילת פעולה שהגדרתם.
עוברים לכרטיסייה Devices (מכשירים), בוחרים את מכשיר הפעולה ואת המאפיין, ומגדירים ערך אחר (לדוגמה, מגדירים את light2's LevelControl (בהירות) ל-50%, כמו שמודגם בצילום המסך הבא:

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

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

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

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

אחרי שהכרתם את תהליך יצירת האוטומציה באפליקציה לדוגמה, אתם יכולים לשלב אוטומציות באפליקציות שלכם.
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 של Automation ושל חיפוש רחב.
אנחנו מקווים שתיהנו מפיתוח אפליקציות ששולטות במכשירים בצורה יצירתית בסביבת Google Home, ומיצירת תרחישי אוטומציה מעניינים באמצעות ממשקי ה-API של Home.
השלבים הבאים
- במאמר פתרון בעיות מוסבר איך לנפות באגים באפליקציות ולפתור בעיות שקשורות לממשקי ה-API של Home.
- אתם יכולים לשלוח לנו המלצות או לדווח על בעיות דרך Issue Tracker בנושא התמיכה בבית חכם.