1. قبل البدء
هذا هو الدرس التطبيقي الثاني في سلسلة حول إنشاء تطبيق Android باستخدام واجهات برمجة التطبيقات Google Home. في هذا الدرس العملي، سنشرح كيفية إنشاء عمليات التشغيل الآلي للمنزل ونقدّم بعض النصائح حول أفضل الممارسات باستخدام واجهات برمجة التطبيقات. إذا لم تكن قد أكملت برنامج التدريب العملي الأول، إنشاء تطبيق متوافق مع الأجهزة الجوّالة باستخدام واجهات برمجة التطبيقات Home على Android، ننصحك بإكماله قبل البدء في برنامج التدريب العملي هذا.
توفّر واجهات برمجة التطبيقات في Google Home مجموعة من المكتبات لمطوّري تطبيقات Android للتحكّم في الأجهزة المنزلية الذكية ضمن المنظومة المتكاملة في Google Home. باستخدام واجهات برمجة التطبيقات الجديدة هذه، سيتمكّن المطوّرون من ضبط عمليات التشغيل الآلي للمنزل الذكي التي يمكنها التحكّم في إمكانات الأجهزة استنادًا إلى شروط محدّدة مسبقًا. توفّر Google أيضًا واجهة برمجة تطبيقات Discovery API التي تتيح لك طلب معلومات من الأجهزة لمعرفة السمات والأوامر التي تتيحها.
المتطلبات الأساسية
- أكمِل الدرس التطبيقي حول الترميز إنشاء تطبيق على الأجهزة الجوّالة باستخدام واجهات برمجة التطبيقات Home على Android.
- معرفة النظام المتكامل لمنتجات Google Home (الربط بين الخدمات السحابية وMatter)
- محطة عمل مثبَّت عليها استوديو Android (الإصدار 2024.3.1 Ladybug أو إصدار أحدث)
- هاتف Android يستوفي متطلبات واجهات برمجة التطبيقات الخاصة بمنزل Google (راجِع المتطلبات الأساسية)، مع تثبيت خدمات Google Play وتطبيق Google Home. لن يعمل المحاكي، بل تتوفّر عيّنة التطبيق على هواتف Android الفعلية فقط.
- Google Home Hub متوافق يتيح استخدام واجهات برمجة التطبيقات في Google Home
- اختياري: جهاز منزلي ذكي متوافق مع واجهات Google Home APIs
أهداف الدورة التعليمية
- كيفية إنشاء عمليات تشغيل آلي للأجهزة المنزلية الذكية باستخدام واجهات برمجة التطبيقات Home
- كيفية استخدام واجهات برمجة التطبيقات Discovery APIs لاستكشاف إمكانات الأجهزة المتوافقة
- كيفية اتّباع أفضل الممارسات عند إنشاء تطبيقاتك باستخدام واجهات برمجة التطبيقات Home APIs
2. إعداد مشروعك
يوضّح الرسم البياني التالي بنية تطبيق يستخدم واجهات برمجة التطبيقات الخاصة بالأجهزة المنزلية:
- رمز التطبيق: هو الرمز الأساسي الذي يعمل عليه المطوّرون لإنشاء واجهة مستخدم التطبيق والمنطق اللازم للتفاعل مع حزمة تطوير البرامج (SDK) لواجهات برمجة التطبيقات في Home.
- حزمة تطوير البرامج (SDK) لواجهات برمجة التطبيقات Home APIs: تعمل حزمة تطوير البرامج (SDK) لواجهات برمجة التطبيقات Home APIs التي توفّرها Google مع خدمة Home APIs في GMSCore للتحكّم في أجهزة المنزل الذكي. يصمّم المطوّرون تطبيقات تعمل مع واجهات برمجة التطبيقات Home من خلال تجميعها مع حزمة تطوير البرامج (SDK) الخاصة بواجهات برمجة التطبيقات Home.
- GMSCore على Android: GMSCore، المعروفة أيضًا باسم "خدمات Google Play"، هي منصة من Google توفّر خدمات النظام الأساسية، ما يتيح الوظائف الرئيسية على جميع أجهزة Android المعتمَدة. تحتوي وحدة "المنزل" في "خدمات Google Play" على الخدمات التي تتفاعل مع واجهات برمجة التطبيقات الخاصة بالمنزل.
في هذا الدرس التطبيقي حول الترميز، سنستند إلى ما تناولناه في إنشاء تطبيق متوافق مع الأجهزة الجوّالة باستخدام واجهات Home API على Android.
تأكَّد من إعداد بنية تتضمّن جهازَين متوافقَين على الأقل وتعمل على الحساب. بما أنّنا سنعمل على إعداد عمليات التشغيل الآلي في هذا الدرس العملي (يؤدي تغيير في حالة الجهاز إلى تشغيل إجراء على جهاز آخر)، ستحتاج إلى جهازَين لرؤية النتائج.
الحصول على نموذج التطبيق
يتوفّر الرمز المصدري للتطبيق النموذجي على 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 على Android. يستند فرع قاعدة الرموز البرمجية هذا إلى ما انتهى إليه الدرس التطبيقي الأول حول الترميز. في هذا الدرس، ستساعدك الأمثلة في التعرّف على كيفية إنشاء عمليات تشغيل آلي. إذا أكملت الدرس العملي السابق وتمكّنت من تشغيل جميع الوظائف، يمكنك اختيار استخدام مشروع "استوديو Android" نفسه لإكمال هذا الدرس العملي بدلاً من استخدام codelab-branch-2
.
بعد تجميع رمز المصدر وتجهيزه للتشغيل على جهازك الجوّال، انتقِل إلى القسم التالي.
3- مزيد من المعلومات عن العمليات المبرمَجة
عمليات التشغيل الآلي هي مجموعة من عبارات "إذا كان هذا، فافعل ذلك" التي يمكنها التحكّم في حالات الأجهزة استنادًا إلى عوامل محدّدة، وبطريقة مبرمَجة. يمكن للمطوّرين استخدام عمليات التشغيل الآلي لإنشاء ميزات تفاعلية متقدّمة في واجهات برمجة التطبيقات.
تتكوّن عمليات التشغيل الآلي من ثلاثة أنواع مختلفة من المكوّنات المعروفة باسم العُقد: أدوات التفعيل والإجراءات والشروط. تعمل هذه العُقد معًا لأتمتة السلوكيات باستخدام الأجهزة المنزلية الذكية. ويتم عادةً تقييمها بالترتيب التالي:
- إجراء التفعيل: يحدّد الشروط الأولية التي تؤدي إلى تفعيل عملية التشغيل الآلي، مثل تغيير قيمة سمة. يجب أن يتضمّن التشغيل الآلي إجراء تفعيل.
- الشرط: أي قيود إضافية يجب تقييمها بعد تشغيل عملية إعداد التقارير المبرمَجة. يجب أن يتم تقييم التعبير في شرط على أنّه صحيح من أجل تنفيذ إجراءات عملية التشغيل الآلي.
- الإجراء: الأوامر أو تعديلات الحالة التي يتم تنفيذها عند استيفاء جميع الشروط.
على سبيل المثال، يمكنك ضبط عملية تشغيل آلي تخفّض إضاءة المصابيح في غرفة معيّنة عند الضغط على مفتاح، بينما يكون التلفزيون في تلك الغرفة قيد التشغيل. في هذا المثال:
- بدء: يتم تبديل مفتاح الإضاءة في الغرفة.
- الشرط: يتم تقييم حالة تشغيل/إيقاف التلفزيون على أنّها "تشغيل".
- الإجراء: يتم خفض مستوى إضاءة المصابيح في الغرفة نفسها التي يوجد فيها مفتاح الإضاءة.
يتم تقييم هذه العُقد من خلال "محرك الأتمتة" إما بشكل تسلسلي أو متوازٍ.
يحتوي التدفّق التسلسلي على عُقد يتم تنفيذها بترتيب تسلسلي. عادةً ما تكون هذه العناصر هي المشغّل والشرط والإجراء.
قد يحتوي التنفيذ المتوازي على عُقد إجراءات متعددة يتم تنفيذها في الوقت نفسه، مثل تشغيل مصابيح متعددة في الوقت نفسه. لن يتم تنفيذ العُقد التي تتّبع مسارًا متوازيًا إلا بعد انتهاء جميع فروع المسار المتوازي.
هناك أنواع أخرى من العُقد في مخطّط التشغيل الآلي. يمكنك الاطّلاع على مزيد من المعلومات عنها في قسم العُقد ضمن "دليل المطوّرين" لواجهات برمجة التطبيقات في Home. بالإضافة إلى ذلك، يمكن للمطوّرين الجمع بين أنواع مختلفة من العُقد لإنشاء عمليات تشغيل آلي معقّدة، مثل ما يلي:
يقدّم المطوّرون هذه العُقد إلى "محرك التشغيل الآلي" باستخدام لغة خاصة بالنطاق (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,
}
ويتم تتبُّع عملية الربط بين القيم التي يحدّدها التطبيق وتلك التي تحدّدها واجهات برمجة التطبيقات:
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)) }
عند تشغيل التطبيق واختيار عملية تشغيل آلي جديدة ومشغّل، من المفترض أن يظهر عرض مشابه لما يلي:
لا يتيح تطبيق Sample App سوى استخدام أدوات بدء التشغيل استنادًا إلى سمات الجهاز.
ضبط الإجراءات
يعكس إجراء التشغيل الآلي الغرض الأساسي من عملية التشغيل الآلي، أي كيفية إحداث تغيير في العالم المادي. في التطبيق التجريبي، نسجّل إجراءات التشغيل الآلي باستخدام الفئة ActionViewModel
، ونعرض طريقة عرض المحرّر باستخدام الفئة ActionView
.
يستخدم التطبيق التجريبي عناصر واجهات برمجة التطبيقات التالية في Home لتحديد عُقد إجراءات التشغيل الآلي:
- الجهاز
- السمة
- 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، كما هو موضّح في لقطة الشاشة التالية:
سنحاول الآن تشغيل عملية التشغيل الآلي باستخدام الجهاز الأوّلي. اختَر جهاز التفعيل الذي حدّدته عند إنشاء عملية التشغيل الآلي. فعِّل السمة التي اخترتها (على سبيل المثال، اضبط starter outlet1
على OnOff
إلى On
):
ستلاحظ أنّ هذا الإجراء ينفّذ أيضًا عملية التشغيل الآلي ويضبط سمة LevelControl
لجهاز الإجراء light2
على القيمة الأصلية، أي %100:
تهانينا، لقد استخدمت واجهات برمجة التطبيقات Home APIs بنجاح لإنشاء عمليات تشغيل آلي.
لمزيد من المعلومات حول Automation API، اطّلِع على Android Automation API.
5- استكشاف الإمكانيات
تتضمّن واجهات برمجة التطبيقات الخاصة بالمنزل واجهة برمجة تطبيقات مخصّصة تُعرف باسم 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.
المرشّحين الذين تعرضهم واجهة برمجة التطبيقات حسب أنواع مختلفة. يتوافق تطبيق Sample App مع 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 وفلترة النتائج التي تتوافق مع Sample App، سنتناول كيفية دمج ذلك في المحرّر.
لمزيد من المعلومات عن 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 الخاصة بالأتمتة. توضّح هذه الأمثلة بعض الإمكانات المتقدّمة التي يمكنك تحقيقها باستخدام واجهات برمجة التطبيقات.
وقت اليوم كبداية
بالإضافة إلى سمات الجهاز، توفّر واجهات برمجة تطبيقات 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 وDiscovery.
نأمل أن تستمتع بإنشاء تطبيقات تتحكّم بشكل مبتكر في الأجهزة ضمن المنظومة المتكاملة Google Home، وأن تنشئ سيناريوهات تشغيل آلي رائعة باستخدام واجهات برمجة التطبيقات Home.
الخطوات التالية
- اطّلِع على تحديد المشاكل وحلّها لمعرفة كيفية تصحيح أخطاء التطبيقات وتحديد المشاكل وحلّها بفعالية باستخدام واجهات برمجة التطبيقات Home.
- يمكنك التواصل معنا لتقديم أي اقتراحات أو الإبلاغ عن أي مشاكل من خلال أداة تتبُّع المشاكل، موضوع دعم "المنزل الذكي".