1. לפני שמתחילים
זהו סדנת הקוד השנייה בסדרה בנושא פיתוח אפליקציה ל-Android באמצעות ממשקי ה-API של Google Home. בקודלאב הזה נסביר איך ליצור אוטומציה ביתית ונציע כמה טיפים לשיטות מומלצות לשימוש בממשקי ה-API. אם עדיין לא השלמת את סדנת הקוד הראשונה, פיתוח אפליקציה לנייד באמצעות ממשקי ה-API של Home ב-Android, מומלץ להשלים אותה לפני שמתחילים את סדנת הקוד הזו.
ממשקי ה-API של Google Home מספקים קבוצה של ספריות למפתחי Android, שמאפשרות להם לשלוט במכשירים לבית חכם בסביבה העסקית של Google Home. בעזרת ממשקי ה-API החדשים האלה, מפתחים יוכלו להגדיר פעולות אוטומטיות לבית חכם שיכולות לשלוט ביכולות של המכשירים על סמך תנאים מוגדרים מראש. Google מספקת גם Discovery API שמאפשר לשלוח שאילתות למכשירים כדי לבדוק אילו מאפיינים ופקודות הם תומכים בהם.
דרישות מוקדמות
- משלימים את הקודלה פיתוח אפליקציה לנייד באמצעות ממשקי ה-API של Home ב-Android.
- ידע בסביבת הבית החכם של Google (מ-Cloud ל-Cloud ו-Matter).
- תחנת עבודה עם Android Studio (גרסה 2024.3.1 Ladybug ואילך) מותקנת.
- טלפון Android שעומד בדרישות של ממשקי ה-API של Home (מפורטות בקטע דרישות מוקדמות), עם שירותי Google Play ואפליקציית Google Home מותקנים.
- Google Home Hub תואם שתומך בממשקי Google Home API.
- אופציונלי – מכשיר בית חכם תואם לממשקי ה-API של Google Home.
מה תלמדו
- איך יוצרים תהליכי אוטומציה למכשירים בבית חכם באמצעות ממשקי ה-API של Home.
- איך משתמשים ב-Discovery APIs כדי לבדוק את יכולות המכשיר הנתמכות.
- איך להשתמש בשיטות המומלצות בזמן פיתוח האפליקציות באמצעות ממשקי ה-API של Home.
2. הגדרת הפרויקט
התרשים הבא מדגים את הארכיטקטורה של אפליקציה עם Home APIs:
- קוד האפליקציה: הקוד המרכזי שמפתחים עובדים עליו כדי ליצור את ממשק המשתמש של האפליקציה ואת הלוגיקה של האינטראקציה עם ה-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: רכיבים להתחלת תהליך, פעולות ותנאים. הצמתים האלה פועלים יחד כדי לבצע אוטומציה של התנהגויות באמצעות מכשירים לבית חכם. בדרך כלל, ההערכה מתבצעת לפי הסדר הבא:
- Starter (התחלה) – מגדיר את התנאים הראשוניים שמפעילים את האוטומציה, כמו שינוי בערך של מאפיין. לכל תהליך אוטומציה צריך להיות Starter.
- תנאי – אילוצים נוספים שצריך להעריך אחרי הפעלת האוטומציה. כדי שהפעולות של האוטומציה יבוצעו, הערך המחושב של הביטוי בתנאי צריך להיות True.
- פעולה – פקודות או עדכוני מצב שמתבצעים כשכל התנאים מתקיימים.
לדוגמה, אפשר ליצור פעולה אוטומטית שמפחיתה את הבהירות של האורות בחדר כשמפעילים מתג, בזמן שהטלוויזיה בחדר מופעלת. בדוגמה הזו:
- Starter – המתג בחדר מופעל.
- Condition (תנאי) – המצב של מצב ההפעלה/השבתה של הטלוויזיה מקבל את הערך 'מופעל'.
- פעולה – התאורה בחדר שבו נמצא המתג תהיה עמומה יותר.
צמתים אלה נבדקים על ידי מנוע האוטומציה באופן טורי או במקביל.
תהליך רצוף מכיל צמתים שפועלים לפי סדר. בדרך כלל, אלה יהיו 'התחלה', 'תנאי' ו'פעולה'.
בתהליך מקביל יכולים להיות כמה צמתים של פעולות שפועלים בו-זמנית, למשל הפעלה של כמה נורות בו-זמנית. צמתים שמשתמשים בתהליך מקביל לא יפעלו עד שכל ההסתעפויות של התהליך המקביל יסתיימו.
יש סוגים אחרים של צמתים בסכימה של האוטומציה. מידע נוסף זמין בקטע Nodes במדריך למפתחים של Home APIs. בנוסף, מפתחים יכולים לשלב סוגים שונים של צמתים כדי ליצור תהליכים אוטומטיים מורכבים, כמו:
המפתחים מספקים את הצמתים האלה למנוע האוטומציה באמצעות שפה ספציפית לדומיין (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. פיתוח עורך של פעולות אוטומטיות
באפליקציית הדוגמה, נוצר עורך אוטומציה שבו המשתמשים יכולים לבחור מכשירים, את היכולות (הפעולות) שבהן הם רוצים להשתמש ואת האופן שבו האוטומציות מופעלות באמצעות 'התחלות'.
הגדרת סימנים לתחילת פעולה
הכלי להתחלת האוטומציה הוא נקודת הכניסה לאוטומציה. טריגר מפעיל פעולה אוטומטית כשאירוע מסוים מתרחש. באפליקציית הדוגמה, אנחנו מתעדים את רכיבי האוטומציה באמצעות הכיתה 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)) }
כשמריצים את האפליקציה ובוחרים אוטומציה וסטארט-אפ חדשים, אמורה להופיע תצוגה כמו זו:
האפליקציה לדוגמה תומכת רק ב-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)) }
הפעלת האפליקציה ובחירה באוטומציה ובפעולה חדשות אמורה להוביל לתצוגה כזו:
באפליקציית הדוגמה אנחנו תומכים רק בפעולות שמבוססות על מאפייני המכשיר.
עיבוד טיוטה של תרחיש אוטומציה
כשה-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, הגיע הזמן לבדוק אם היא פועלת גם באמצעות ה-starter שהגדרתם.
עוברים לכרטיסייה 'מכשירים', בוחרים את מכשיר הפעולה ואת המאפיין ומגדירים לו ערך אחר (לדוגמה, מגדירים את LevelControl
(בהירות) של light2
ל-50%, כפי שמוצג בצילום המסך הבא:
עכשיו ננסה להפעיל את האוטומציה באמצעות מכשיר ההתחלה. בוחרים את המכשיר להתחלת הפעולה שבחרתם כשיצרתם את האוטומציה. משנים את המאפיין שבחרתם (לדוגמה, מגדירים את OnOff
של starter outlet1
כ-On
):
תוכלו לראות שהפעולה הזו גם מבצעת את האוטומציה ומגדירה את המאפיין LevelControl
של מכשיר הפעולה light2
לערך המקורי, 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 כדי להפעיל את הפונקציה .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, כפי שמוצג בקטע הקודם. תוצג בקשה ליצור תהליך אוטומציה חדש על סמך הפקודה של המועמד:
עכשיו, אחרי שלמדתם איך ליצור אוטומציה באפליקציית הדוגמה, תוכלו לשלב אוטומציות באפליקציות שלכם.
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.