1. قبل البدء
هذا هو الدرس التطبيقي الثاني في سلسلة عن إنشاء تطبيق Android باستخدام واجهات برمجة تطبيقات Google Home. في هذا الدليل التعليمي، سنشرح كيفية إنشاء أنظمة التشغيل الآلي للمنزل وسنقدّم بعض النصائح حول أفضل الممارسات لاستخدام واجهات برمجة التطبيقات. إذا لم تكن قد أكملت دورة codelab الأولى، إنشاء تطبيق متوافق مع الأجهزة الجوّالة باستخدام واجهات برمجة تطبيقات Home API على Android، ننصحك بإكمالها قبل بدء دورة codelab هذه.
توفّر واجهات برمجة تطبيقات Google Home مجموعة من المكتبات لمطوّري تطبيقات Android للتحكّم في الأجهزة المنزلية الذكية ضمن المنظومة المتكاملة لأجهزة Google Home. باستخدام واجهات برمجة التطبيقات الجديدة هذه، سيتمكّن المطوّرون من ضبط عمليات التشغيل الآلي للمنزل الذكي التي يمكنها التحكّم في إمكانات الأجهزة استنادًا إلى شروط محدّدة مسبقًا. توفّر Google أيضًا واجهة برمجة التطبيقات Discovery API التي تتيح لك طلب معلومات من الأجهزة لمعرفة السمات والأوامر التي تتيحها.
المتطلبات الأساسية
- أكمِل الدرس التطبيقي إنشاء تطبيق جوّال باستخدام واجهات برمجة تطبيقات Home على Android.
- معرفة بالمنظومة المتكاملة لأجهزة Google Home (الربط بين السحابة الإلكترونية وMatter)
- محطة عمل تم تثبيت Android Studio (الإصدار 2024.3.1 Ladybug أو إصدار أحدث) عليها
- هاتف Android يستوفي متطلبات واجهات برمجة تطبيقات Home (راجِع المتطلّبات الأساسية)، ومزوّد بخدمات Google Play وتطبيق Google Home
- Google Home Hub متوافق مع Google Home APIs
- اختياري: جهاز منزلي ذكي متوافق مع واجهات برمجة تطبيقات Google Home
المُعطيات
- كيفية إنشاء عمليات تشغيل آلي لأجهزة المنزل الذكية باستخدام Home APIs
- كيفية استخدام Discovery APIs لاستكشاف إمكانات الأجهزة المتوافقة
- كيفية تطبيق أفضل الممارسات عند إنشاء تطبيقاتك باستخدام Home APIs
2. إعداد مشروعك
يوضّح الرسم البياني التالي بنية تطبيق Home APIs:
- رمز التطبيق: هو الرمز الأساسي الذي يعمل عليه المطوّرون لإنشاء واجهة مستخدم التطبيق ومنطق التفاعل مع حزمة تطوير البرامج (SDK) لواجهات برمجة تطبيقات Home.
- حزمة تطوير البرامج (SDK) لواجهات برمجة التطبيقات Home APIs: تعمل حزمة تطوير البرامج (SDK) لواجهات برمجة التطبيقات Home APIs التي تقدّمها Google مع خدمة Home APIs في GMSCore للتحكّم في أجهزة المنزل الذكي. ينشئ المطوّرون تطبيقات تعمل مع واجهات برمجة تطبيقات Home من خلال تجميعها مع حزمة تطوير البرامج (SDK) الخاصة بـ Home APIs.
- GMSCore على Android: GMSCore، المعروفة أيضًا باسم "خدمات Google Play"، هي منصة من Google توفّر خدمات النظام الأساسية، ما يتيح وظائف رئيسية على جميع أجهزة Android المعتمَدة. تحتوي وحدة Home في "خدمات Google Play" على الخدمات التي تتفاعل مع واجهات برمجة تطبيقات Home.
في هذا الدرس التطبيقي حول الترميز، سنستند إلى ما غطّيناه في مقالة إنشاء تطبيق جوّال باستخدام واجهات برمجة التطبيقات Home APIs على Android.
تأكَّد من توفُّر بنية تتضمّن جهازَين متوافقَين على الأقل تم إعدادهما واستخدامهما في الحساب. بما أنّنا سنُعدّ عمليات التشغيل الآلي في هذا الدليل التعليمي (يؤدي تغيير في حالة أحد الأجهزة إلى تنفيذ إجراء على جهاز آخر)، ستحتاج إلى جهازَين للاطّلاع على النتائج.
الحصول على تطبيق Sample
يتوفّر رمز المصدر لتطبيق "عيّنة التطبيق" على GitHub في مستودع google-home/google-home-api-sample-app-android.
يستخدم هذا الدرس التطبيقي الأمثلة من فرع 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 APIs على Android. يستند هذا الفرع من قاعدة البيانات إلى النقطة التي توقّف عندها الدرس التطبيقي الأول حول الترميز. وتشرح لك الأمثلة هذه المرة كيفية إنشاء عمليات التشغيل الآلي. إذا أكملت دورة تعلم البرمجة السابقة وتمكّنت من تفعيل جميع الوظائف، يمكنك اختيار استخدام مشروع "استوديو Android" نفسه لإكمال دورة تعلم البرمجة هذه بدلاً من استخدام codelab-branch-2
.
بعد تجميع رمز المصدر واستعداده للتشغيل على جهازك الجوّال، انتقِل إلى القسم التالي.
3- التعرّف على عمليات التشغيل الآلي
عمليات التشغيل الآلي هي مجموعة من عبارات "إذا حدث هذا، فينطبق ذلك" التي يمكنها التحكّم في حالات الأجهزة استنادًا إلى عوامل محدّدة، بطريقة آلية. يمكن للمطوّرين استخدام عمليات التشغيل الآلي لإنشاء ميزات تفاعلية متقدّمة في واجهات برمجة التطبيقات.
تتكوّن الإجراءات المبرمَجة من ثلاثة أنواع مختلفة من المكوّنات المعروفة باسم nodes: إجراءات التفعيل والإجراءات والشروط. تعمل هذه العقد معًا لتشغيل السلوكيات تلقائيًا باستخدام الأجهزة المنزلية الذكية. ويتم تقييمها عادةً بالترتيب التالي:
- Starter: يحدّد الشروط الأولية التي تفعّل التشغيل الآلي، مثل تغيير في قيمة سمة. يجب أن يكون للإجراءات المبرمَجة Starter.
- الحالة: أي قيود إضافية لتقييمها بعد بدء عملية التشغيل الآلي. يجب أن يتم تقييم التعبير في الحالة على أنّه صحيح لتنفيذ إجراءات التشغيل الآلي.
- الإجراء: الأوامر أو تعديلات الحالة التي يتم تنفيذها عند استيفاء جميع الشروط
على سبيل المثال، يمكنك إعداد عملية تشغيل آلي تخفض مستوى الإضاءة في غرفة عند تشغيل مفتاح، بينما يكون التلفزيون في تلك الغرفة مفعّلاً. في هذا المثال:
- Starter: تم تبديل "المفتاح" في الغرفة.
- الحالة: يتم تقييم حالة تشغيل/إيقاف التلفزيون على أنّها "تشغيل".
- الإجراء: يتم إطفاء الإضاءة في الغرفة نفسها التي يوجد فيها مفتاح التبديل.
يُقيّم محرّك التشغيل الآلي هذه العقد إما بشكل تسلسلي أو موازٍ.
يحتوي المسار التسلسلي على عقد يتم تنفيذها بترتيب تسلسلي. وعادةً ما تكون هذه العناصر هي المشغِّل والحالة والإجراء.
قد يتضمّن المسار المتوازي عدة عقد إجراءات يتم تنفيذها في الوقت نفسه، مثل تشغيل عدة مصابيح في الوقت نفسه. لن يتم تنفيذ العقد التي تتّبع تدفّقًا موازًٍا إلى أن تنتهي جميع فروع التدفّق الموازي.
هناك أنواع أخرى من العقد في مخطّط التشغيل الآلي. يمكنك الاطّلاع على مزيد من المعلومات حولها في قسم العناصر من دليل مطوّري واجهة برمجة التطبيقات 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,
}
وتتتبّع عملية الربط بين القيم التي يحدّدها التطبيق والقيم التي تحدّدها واجهات برمجة التطبيقات:
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
لعرض جميع الأجهزة المبتدئة وتنفيذ ردّ الاتصال بالنقر في 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
لعرض جميع سمات الجهاز المشغِّل وتنفيذ ردّ الاتصال بالنقر في 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
لعرض جميع عمليات السمة المحدّدة وتنفيذ ردّ الاتصال بالنقر في 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
لعرض جميع قيم السمة المحدّدة وتنفيذ ردّ الاتصال بالنقر في 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 التالية لتحديد عقد إجراءات التشغيل الآلي:
- الجهاز
- السمة
- Command
- القيمة (اختيارية)
يستخدم كل إجراء من إجراءات أوامر الجهاز أمرًا، ولكن سيتطلّب بعضها أيضًا قيمة مَعلمة مرتبطة به، مثل 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?>
تعرِض واجهة برمجة التطبيقات مجموعة من عناصر العرض التي يمكن للمستخدمين استخدامها لاختيار الحقول المطلوبة.
أزِل التعليق التوضيحي عن الخطوة 4.2.1 في ملف ActionView.kt
لعرض جميع أجهزة الإجراءات وتنفيذ ردّ الاتصال بالنقر في 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
وتنفيذ ردّ الاتصال بالنقر في 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
وتنفيذ ردّ الاتصال بالنقرة في 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. تحتوي واجهة برمجة التطبيقات على دالة 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
لإنشاء عملية التشغيل الآلي من خلال استدعاء واجهات برمجة تطبيقات 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
}
}
الآن، يمكنك تشغيل التطبيق والاطّلاع على التغييرات على جهازك.
بعد اختيار إجراء التفعيل والإجراء، تكون مستعدًا لإنشاء الإجراء المبرمَج:
احرص على اختيار اسم فريد للنظام الآلي، ثم انقر على الزر إنشاء نظام آلي الذي من المفترض أن يستدعي واجهات برمجة التطبيقات ويعيدك إلى عرض قائمة الأنظمة الآلية مع النظام الآلي الذي أنشأته:
انقر على عملية التشغيل الآلي التي أنشأتها للتو، واطّلِع على كيفية عرضها من خلال واجهات برمجة التطبيقات.
يُرجى العِلم أنّ واجهة برمجة التطبيقات تعرض قيمة تشير إلى ما إذا كانت عملية التشغيل الآلي صالحة ونشطة حاليًا أم لا. من الممكن إنشاء إجراءات آلية لا تجتاز عملية التحقّق عند تحليلها من جهة الخادم. إذا تعذّر التحقّق من صحة تحليل التشغيل الآلي، يتم ضبط isValid
على false
، ما يشير إلى أنّ التشغيل الآلي غير صالح وغير نشط. إذا كانت عملية التشغيل الآلي غير صالحة، اطّلِع على حقل automation.validationIssues
للاطّلاع على التفاصيل.
تأكَّد من ضبط عملية التشغيل الآلي على أنّها صالحة ونشطة، ثم يمكنك تجربة عملية التشغيل الآلي.
تجربة الحلول البرمجية
يمكن تنفيذ الإجراءات المبرمَجة بطريقتَين:
- باستخدام حدث بدء. في حال تطابق الشروط، يؤدي ذلك إلى بدء الإجراء الذي ضبطته في عملية التشغيل الآلي.
- من خلال طلب بيانات من واجهة برمجة التطبيقات للتنفيذ اليدوي
إذا كانت مسودة التشغيل الآلي تحتوي على manualStarter()
محدّد في كتلة DSL لمسودة التشغيل الآلي، سيتيح محرّك التشغيل الآلي التنفيذ اليدوي لهذا التشغيل الآلي. يتوفّر هذا الرمز في أمثلة الرموز البرمجية في "التطبيق التجريبي".
بما أنّك لا تزال على شاشة عرض التشغيل الآلي على جهازك الجوّال، انقر على الزر تنفيذ يدوي. من المفترض أن يؤدي ذلك إلى استدعاء automation.execute()
الذي ينفِّذ أمر الإجراء على الجهاز الذي اخترته عند إعداد الإجراء المبرمَج.
بعد التحقّق من صحة أمر الإجراء من خلال التنفيذ اليدوي باستخدام واجهة برمجة التطبيقات، حان الوقت لمعرفة ما إذا كان سيتم تنفيذه أيضًا باستخدام أداة البدء التي حدّدتها.
انتقِل إلى علامة التبويب "الأجهزة"، واختَر جهاز الإجراء والسمة، واضبطها على قيمة مختلفة (على سبيل المثال، اضبط light2
على LevelControl
(السطوع) على %50، كما هو موضّح في لقطة الشاشة التالية:
سنحاول الآن بدء التشغيل التلقائي باستخدام جهاز التشغيل. اختَر جهاز التفعيل الذي اخترته عند إنشاء عملية التشغيل الآلي. فعِّل السمة التي اخترتها (على سبيل المثال، اضبط OnOff
في starter outlet1
على On
):
ستلاحظ أنّ هذا الإجراء يؤدي أيضًا إلى تنفيذ التشغيل الآلي وضبط سمة LevelControl
لجهاز الإجراء light2
على القيمة الأصلية، وهي %100:
تهانينا، لقد استخدمت بنجاح واجهات برمجة تطبيقات Home لإنشاء عمليات التشغيل الآلي.
للاطّلاع على مزيد من المعلومات عن Automation API، اطّلِع على Android Automation API.
5- التعرّف على الإمكانات
تتضمّن Home APIs واجهة برمجة تطبيقات مخصّصة تُعرف باسم Discovery 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.
. تنتمي المرشحون الذين تعرضهم واجهة برمجة التطبيقات إلى أنواع مختلفة. يتيح "نموذج التطبيق" استخدام 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. أمثلة على النماذج الآلية المتقدّمة
قبل الختام، سنناقش بعض الأمثلة الإضافية على لغة وصف الجلسة للتشغيل الآلي. توضّح هذه الأمثلة بعض الإمكانات المتقدّمة التي يمكنك تحقيقها باستخدام واجهات برمجة التطبيقات.
وقت اليوم كعامل بدء
بالإضافة إلى سمات الجهاز، تقدّم واجهات برمجة تطبيقات 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
...
}
}
بث "مساعد Google" كإجراء
تتوفّر السمة AssistantBroadcast
إما كسمة على مستوى الجهاز في SpeakerDevice
(إذا كان مكبّر الصوت يتيح ذلك) أو كسمة على مستوى البنية (لأنّ مكبّرات صوت Google والأجهزة الجوّالة التي تعمل بنظام التشغيل Android يمكنها تشغيل عمليات بث "مساعد Google"). على سبيل المثال:
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
هي سمة على مستوى البنية ترصد ما إذا كان هناك أي شخص في المنزل.
على سبيل المثال، يوضّح المثال التالي قفل الأبواب تلقائيًا عندما يكون أحد الأشخاص في المنزل بعد الساعة 10 مساءً:
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. خلال هذا الدرس التطبيقي، استكشَفت Automation API وDiscovery API.
نأمل أن تستمتع بإنشاء تطبيقات تتيح التحكّم بشكل إبداعي في الأجهزة ضمن المنظومة المتكاملة لأجهزة Google Home وإنشاء سيناريوهات تشغيل آلي مثيرة باستخدام واجهات برمجة تطبيقات Home.
الخطوات التالية
- اطّلِع على تحديد المشاكل وحلّها للتعرّف على كيفية تصحيح أخطاء التطبيقات بفعالية وتحديد المشاكل وحلّها في ما يتعلّق بواجهة برمجة التطبيقات Home APIs.
- يمكنك التواصل معنا لتقديم أي اقتراحات أو الإبلاغ عن أي مشاكل من خلال أداة تتبُّع المشاكل في موضوع دعم "المنزل الذكي".