با استفاده از APIهای Home در Android، اتوماسیون ایجاد کنید

۱. قبل از شروع

این دومین آزمایشگاه کد از سری آموزش‌های ساخت اپلیکیشن اندروید با استفاده از APIهای گوگل هوم است. در این آزمایشگاه کد، نحوه ایجاد اتوماسیون خانگی را بررسی می‌کنیم و نکاتی را در مورد بهترین شیوه‌های استفاده از APIها ارائه می‌دهیم. اگر هنوز اولین آزمایشگاه کد، «ساخت اپلیکیشن موبایل با استفاده از APIهای هوم در اندروید» را تکمیل نکرده‌اید، توصیه می‌کنیم قبل از شروع این آزمایشگاه کد، آن را تکمیل کنید.

رابط‌های برنامه‌نویسی کاربردی گوگل هوم مجموعه‌ای از کتابخانه‌ها را برای توسعه‌دهندگان اندروید فراهم می‌کنند تا دستگاه‌های خانه هوشمند را در اکوسیستم گوگل هوم کنترل کنند. با استفاده از این رابط‌های برنامه‌نویسی کاربردی جدید، توسعه‌دهندگان قادر خواهند بود اتوماسیون‌هایی را برای یک خانه هوشمند تنظیم کنند که بتواند قابلیت‌های دستگاه را بر اساس شرایط از پیش تعریف‌شده کنترل کند. گوگل همچنین یک رابط برنامه‌نویسی کاربردی اکتشافی (Discovery API) ارائه می‌دهد که به شما امکان می‌دهد دستگاه‌ها را جستجو کنید تا دریابید که از چه ویژگی‌ها و دستوراتی پشتیبانی می‌کنند.

این ویدیو خلاصه‌ای از اتوماسیون‌هایی که ایجاد خواهید کرد را ارائه می‌دهد، بنابراین همزمان با انجام آزمایش کد، ویدیو را دنبال کنید.

پیش‌نیازها

آنچه یاد خواهید گرفت

  • نحوه ایجاد اتوماسیون برای دستگاه‌های خانه هوشمند با استفاده از APIهای خانه.
  • نحوه استفاده از APIهای Discovery برای بررسی قابلیت‌های دستگاه پشتیبانی‌شده.
  • چگونه می‌توان بهترین شیوه‌ها را هنگام ساخت برنامه‌های خود با APIهای Home به کار گرفت.

۲. راه‌اندازی پروژه

نمودار زیر معماری یک برنامه Home API را نشان می‌دهد:

معماری رابط‌های برنامه‌نویسی کاربردی (API) صفحه اصلی برای یک برنامه اندروید

  • کد برنامه: کد اصلی که توسعه‌دهندگان برای ساخت رابط کاربری برنامه و منطق تعامل با SDK رابط‌های برنامه‌نویسی کاربردی خانگی (Home APIs) روی آن کار می‌کنند.
  • کیت توسعه نرم‌افزاری رابط‌های برنامه‌نویسی کاربردی (SDK) رابط‌های برنامه‌نویسی کاربردی ...
  • GMSCore در اندروید: GMSCore که با نام سرویس‌های گوگل پلی نیز شناخته می‌شود، یک پلتفرم گوگل است که سرویس‌های سیستم اصلی را ارائه می‌دهد و قابلیت‌های کلیدی را در تمام دستگاه‌های اندروید دارای گواهینامه فعال می‌کند. ماژول خانگی سرویس‌های گوگل پلی شامل سرویس‌هایی است که با APIهای خانگی تعامل دارند.

در این آزمایشگاه کد، بر اساس مطالبی که در بخش «ساخت یک برنامه موبایل با استفاده از رابط‌های برنامه‌نویسی کاربردی Home در اندروید» پوشش دادیم، کدنویسی خواهیم کرد.

مطمئن شوید که ساختاری دارید که حداقل دو دستگاه پشتیبانی‌شده روی آن تنظیم شده و روی حساب کاربری کار می‌کنند. از آنجایی که قرار است در این آزمایشگاه کد، اتوماسیون را راه‌اندازی کنیم (تغییر در وضعیت یک دستگاه، عملی را روی دستگاه دیگر آغاز می‌کند)، برای دیدن نتایج به دو دستگاه نیاز خواهید داشت.

دریافت نمونه برنامه

کد منبع برنامه نمونه در گیت‌هاب و در مخزن 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 در اندروید استفاده می‌شود، متفاوت است. این شاخه از کدبیس، بر اساس جایی که اولین codelab متوقف شده بود، ساخته می‌شود. این بار، مثال‌ها شما را در نحوه ایجاد اتوماسیون راهنمایی می‌کنند. اگر codelab قبلی را تکمیل کرده‌اید و توانستید تمام عملکردها را به کار بیندازید، می‌توانید به جای استفاده از codelab-branch-2 از همان پروژه اندروید استودیو برای تکمیل این codelab استفاده کنید.

وقتی کد منبع را کامپایل کردید و آماده اجرا روی دستگاه تلفن همراه خود شدید، به بخش بعدی بروید.

۳. در مورد اتوماسیون‌ها اطلاعات کسب کنید

اتوماسیون‌ها مجموعه‌ای از عبارات «اگر این، آنگاه آن» هستند که می‌توانند وضعیت دستگاه را بر اساس عوامل انتخاب شده، به صورت خودکار کنترل کنند. توسعه‌دهندگان می‌توانند از اتوماسیون‌ها برای ساخت ویژگی‌های تعاملی پیشرفته در APIهای خود استفاده کنند.

اتوماسیون‌ها از سه نوع مؤلفه مختلف به نام گره‌ها تشکیل شده‌اند: شروع‌کننده‌ها، اقدامات و شرایط. این گره‌ها با هم کار می‌کنند تا رفتارها را با استفاده از دستگاه‌های خانه هوشمند خودکار کنند. معمولاً، آنها به ترتیب زیر ارزیابی می‌شوند:

  1. شروع‌کننده - شرایط اولیه‌ای را که اتوماسیون را فعال می‌کنند، مانند تغییر در مقدار یک ویژگی، تعریف می‌کند. یک اتوماسیون باید یک شروع‌کننده داشته باشد.
  2. شرط (Condition) — هرگونه محدودیت اضافی که پس از فعال شدن یک اتوماسیون باید ارزیابی شود. عبارت موجود در یک شرط (Condition) باید به مقدار درست (true) ارزیابی شود تا اقدامات یک اتوماسیون اجرا شوند.
  3. اقدام (Action) — دستورات یا به‌روزرسانی‌های وضعیت که زمانی انجام می‌شوند که همه شرایط برآورده شده باشند.

برای مثال، می‌توانید یک سیستم اتوماسیون داشته باشید که وقتی یک کلید زده می‌شود، چراغ‌های یک اتاق را کم‌نور کند، در حالی که تلویزیون آن اتاق روشن است. در این مثال:

  • شروع کننده - کلید برق در اتاق فعال است.
  • وضعیت — وضعیت روشن/خاموش تلویزیون، روشن ارزیابی می‌شود.
  • اقدام - چراغ‌های همان اتاقی که سوییچ در آن قرار دارد، کم‌نور می‌شوند.

این گره‌ها توسط موتور اتوماسیون به صورت سریال یا موازی ارزیابی می‌شوند.

تصویر5.png

یک جریان ترتیبی شامل گره‌هایی است که به ترتیب اجرا می‌شوند. معمولاً این گره‌ها عبارتند از شروع‌کننده، شرط و عمل.

تصویر6.png

یک جریان موازی ممکن است چندین گره عملیاتی داشته باشد که به طور همزمان اجرا می‌شوند، مانند روشن کردن چندین چراغ به طور همزمان. گره‌هایی که از یک جریان موازی پیروی می‌کنند تا زمانی که تمام شاخه‌های جریان موازی تمام نشوند، اجرا نخواهند شد.

انواع دیگری از گره‌ها در طرح اتوماسیون وجود دارند. می‌توانید در بخش گره‌ها در راهنمای توسعه‌دهندگان Home APIs درباره آنها بیشتر بدانید. علاوه بر این، توسعه‌دهندگان می‌توانند انواع مختلف گره‌ها را برای ایجاد اتوماسیون‌های پیچیده، مانند موارد زیر، ترکیب کنند:

تصویر13.png

توسعه‌دهندگان این گره‌ها را با استفاده از یک زبان خاص دامنه (DSL) که به‌طور خاص برای اتوماسیون‌های گوگل هوم ایجاد شده است، در اختیار موتور اتوماسیون قرار می‌دهند.

اتوماسیون DSL را بررسی کنید

زبان مختص دامنه (DSL) زبانی است که برای ثبت رفتار سیستم در کد استفاده می‌شود. کامپایلر کلاس‌های داده‌ای تولید می‌کند که به بافر پروتکل JSON سریالی می‌شوند و برای برقراری تماس با سرویس‌های اتوماسیون گوگل استفاده می‌شوند.

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() ).

وقتی با اتوماسیون کار می‌کنید، بدانید که محدودیت‌های منابع وجود دارد.

اتوماسیون‌ها ابزاری بسیار مفید برای ایجاد قابلیت‌های خودکار در یک خانه هوشمند هستند. در ابتدایی‌ترین حالت استفاده، می‌توانید به طور صریح یک اتوماسیون را برای استفاده از دستگاه‌ها و ویژگی‌های خاص کدنویسی کنید. اما یک حالت استفاده کاربردی‌تر، حالتی است که برنامه به کاربر اجازه می‌دهد دستگاه‌ها، دستورات و پارامترهای یک اتوماسیون را پیکربندی کند. بخش بعدی نحوه ایجاد یک ویرایشگر اتوماسیون را توضیح می‌دهد که به کاربر اجازه می‌دهد دقیقاً همین کار را انجام دهد.

۴. یک ویرایشگر اتوماسیون بسازید

در داخل برنامه نمونه، ما یک ویرایشگر اتوماسیون ایجاد خواهیم کرد که کاربران می‌توانند با استفاده از آن، دستگاه‌ها، قابلیت‌ها (اقدامات) مورد نظر خود و نحوه فعال شدن اتوماسیون‌ها را با استفاده از آغازگرها انتخاب کنند.

img11-01.pngimg11-02.pngimg11-03.pngimg11-04.png

راه‌اندازی استارترها

شروع‌کننده‌ی اتوماسیون، نقطه‌ی ورود به اتوماسیون است. یک شروع‌کننده، زمانی که یک رویداد مشخص رخ می‌دهد، اتوماسیون را فعال می‌کند. در برنامه‌ی نمونه، شروع‌کننده‌های اتوماسیون را با استفاده از کلاس StarterViewModel که در فایل منبع StarterViewModel.kt یافت می‌شود، ثبت می‌کنیم و نمای ویرایشگر را با استفاده از StarterView ( StarterView.kt ) نمایش می‌دهیم.

یک گره شروع کننده به عناصر زیر نیاز دارد:

  • دستگاه
  • صفت
  • عملیات
  • ارزش

دستگاه و ویژگی را می‌توان از اشیاء برگردانده شده توسط 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
))

به طور مشابه، برنامه Sample مقادیر قابل انتساب به صفات را پیگیری می‌کند:

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,
)

سپس برنامه مجموعه‌ای از عناصر نمایش را نمایش می‌دهد که کاربران می‌توانند از آنها برای انتخاب فیلدهای مورد نیاز استفاده کنند.

مرحله ۴.۱.۱ در فایل 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
//         }
//     )
// }
}

مرحله ۴.۱.۲ در فایل 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
//         }
//     )
}
}

مرحله ۴.۱.۳ در فایل 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
    //          }
    //      )
    //  }
}

مرحله ۴.۱.۴ در فایل 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 -> {
              ...
      }
   }

مرحله ۴.۱.۵ در فایل 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)) }

اجرای برنامه و انتخاب یک اتوماسیون و شروع‌کننده جدید باید نمایی مانند زیر را نشان دهد:

79beb3b581ec71ec.png

برنامه نمونه فقط از برنامه‌های اولیه بر اساس ویژگی‌های دستگاه پشتیبانی می‌کند.

اقدامات را تنظیم کنید

عمل اتوماسیون، هدف اصلی یک اتوماسیون، یعنی چگونگی تأثیر آن بر تغییر در دنیای فیزیکی را منعکس می‌کند. در برنامه نمونه، ما اقدامات اتوماسیون را با استفاده از کلاس ActionViewModel ثبت می‌کنیم و نمای ویرایشگر را با استفاده از کلاس ActionView نمایش می‌دهیم.

برنامه نمونه از موجودیت‌های APIهای خانگی زیر برای تعریف گره‌های اقدام اتوماسیون استفاده می‌کند:

  • دستگاه
  • صفت
  • فرمان
  • مقدار (اختیاری)

هر دستور دستگاه از یک دستور استفاده می‌کند، اما برخی از آنها به یک مقدار پارامتر مرتبط با آن نیز نیاز دارند، مانند 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 مجموعه‌ای از عناصر نمایشی را نمایش می‌دهد که کاربران می‌توانند از آنها برای انتخاب فیلدهای مورد نیاز استفاده کنند.

مرحله ۴.۲.۱ در فایل 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
//         }
//     )
// }
}

مرحله ۴.۲.۲ در فایل 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
//         }
//     )
// }
}

مرحله ۴.۲.۳ در فایل 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
//         }
//     )
// }
}

مرحله ۴.۲.۴ در فایل ActionView.kt را از حالت کامنت خارج کنید تا مقادیر موجود در action (دستور) مربوط به trait (trait) رندر شوند و مقدار در 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
//      )
//  }
...
}

مرحله ۴.۲.۵ در فایل ActionView.kt را از حالت کامنت خارج کنید تا تمام متغیرهای action ViewModel در action ViewModel اتوماسیون draft ( draftVM.actionVMs ) ذخیره شوند:

val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save action button:
Button(
  enabled = isOptionsSelected,
  onClick = {
  scope.launch {
  // TODO: 4.2.5 - store all action ViewModel variables into draft ViewModel
  // actionVM.deviceVM.emit(actionDeviceVM.value)
  // actionVM.trait.emit(actionTrait.value)
  // actionVM.action.emit(actionAction.value)
  // actionVM.valueLevel.emit(actionValueLevel.value)
  //
  // draftVM.actionVMs.value.add(actionVM)
  // draftVM.selectedActionVM.emit(null)
  }
})
{ Text(stringResource(R.string.action_button_create)) }

اجرای برنامه و انتخاب یک اتوماسیون و اقدام جدید باید منجر به نمایی مانند زیر شود:

6efa3c7cafd3e595.png

ما فقط از اقدامات مبتنی بر ویژگی‌های دستگاه در برنامه نمونه پشتیبانی می‌کنیم.

ارائه یک اتوماسیون پیش‌نویس

وقتی DraftViewModel کامل شد، می‌تواند توسط HomeAppView.kt رندر شود:

fun HomeAppView (homeAppVM: HomeAppViewModel) {
  ...
  // If a draft automation is selected, show the draft editor:
  if (selectedDraftVM != null) {
    DraftView(homeAppVM)
  }
  ...
}

در DraftView.kt :

fun DraftView (homeAppVM: HomeAppViewModel) {
   val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
    ...
// Draft Starters:
   DraftStarterList(draftVM)
// Draft Actions:
   DraftActionList(draftVM)
}

ایجاد یک اتوماسیون

حالا که یاد گرفتید چگونه شروع‌کننده‌ها و اقدامات را ایجاد کنید، آماده‌اید تا یک پیش‌نویس اتوماسیون ایجاد کنید و آن را به API اتوماسیون ارسال کنید. این API دارای تابعی به createAutomation() است که یک پیش‌نویس اتوماسیون را به عنوان آرگومان دریافت می‌کند و یک نمونه اتوماسیون جدید برمی‌گرداند.

آماده‌سازی خودکارسازی پیش‌نویس در کلاس DraftViewModel در برنامه نمونه انجام می‌شود. برای کسب اطلاعات بیشتر در مورد نحوه ساختاردهی پیش‌نویس خودکار با استفاده از متغیرهای starter و action در بخش قبل، به تابع getDraftAutomation() نگاهی بیندازید.

مرحله ۴.۴.۱ در فایل 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()
     ...
}

مرحله ۴.۴.۲ در فایل 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.

مرحله ۴.۴.۳ در فایل HomeAppViewModel.kt را از حالت کامنت خارج کنید تا با فراخوانی APIهای Home و مدیریت استثنائات، اتوماسیون ایجاد شود:

fun createAutomation(isPending: MutableState<Boolean>) {
  viewModelScope.launch {
    val structure : Structure = selectedStructureVM.value?.structure!!
    val draft : DraftAutomation = selectedDraftVM.value?.getDraftAutomation()!!
    isPending.value = true
    // TODO: 4.4.3 - Call the Home API to create automation and handle exceptions
    // // Call Automation API to create an automation from a draft:
    // try {
    //     structure.createAutomation(draft)
    // }
    // catch (e: Exception) {
    //     MainActivity.showError(this, e.toString())
    //     isPending.value = false
    //     return@launch
    // }
    // Scrap the draft and automation candidates used in the process:
    selectedCandidateVMs.emit(null)
    selectedDraftVM.emit(null)
    isPending.value = false
  }
}

حالا برنامه را اجرا کنید و تغییرات را روی دستگاه خود مشاهده کنید!

پس از انتخاب یک شروع‌کننده و اقدام، آماده ایجاد اتوماسیون هستید:

ec551405f8b07b8e.png

مطمئن شوید که نام اتوماسیون خود را منحصر به فرد انتخاب کرده‌اید، سپس روی دکمه‌ی «ایجاد اتوماسیون» ضربه بزنید، که باید APIها را فراخوانی کند و شما را به نمای لیست اتوماسیون‌ها با اتوماسیون شما بازگرداند:

8eebc32cd3755618.png

روی اتوماسیونی که تازه ایجاد کرده‌اید ضربه بزنید و ببینید که چگونه توسط APIها بازگردانده می‌شود.

931dba7c325d6ef7.png

توجه داشته باشید که API مقداری را برمی‌گرداند که نشان می‌دهد آیا یک اتوماسیون معتبر و در حال حاضر فعال است یا خیر . می‌توان اتوماسیون‌هایی ایجاد کرد که هنگام تجزیه در سمت سرور، اعتبارسنجی را با موفقیت پشت سر نگذارند. اگر تجزیه اتوماسیون اعتبارسنجی را با شکست مواجه کند، isValid روی false تنظیم می‌شود که نشان می‌دهد اتوماسیون نامعتبر و غیرفعال است. اگر اتوماسیون شما نامعتبر است، برای جزئیات بیشتر، فیلد automation.validationIssues را بررسی کنید.

مطمئن شوید که اتوماسیون شما معتبر و فعال است، و سپس می‌توانید آن را امتحان کنید.

اتوماسیون خود را امتحان کنید

اتوماسیون‌ها می‌توانند به دو صورت اجرا شوند:

  1. با یک رویداد آغازین. اگر شرایط مطابقت داشته باشند، این رویداد، عملی را که در اتوماسیون تنظیم کرده‌اید، فعال می‌کند.
  2. با یک فراخوانی API برای اجرای دستی.

اگر یک اتوماسیون پیش‌نویس دارای یک manualStarter() تعریف شده در بلوک DSL پیش‌نویس اتوماسیون باشد، موتور اتوماسیون از اجرای دستی برای آن اتوماسیون پشتیبانی می‌کند. این مورد در حال حاضر در نمونه‌های کد در برنامه نمونه وجود دارد.

از آنجا که هنوز در صفحه نمایش اتوماسیون در دستگاه همراه خود هستید، روی دکمه Manual Execute ضربه بزنید. این کار باید automation.execute() را فراخوانی کند، که دستور اکشن شما را روی دستگاهی که هنگام تنظیم اتوماسیون انتخاب کرده‌اید، اجرا می‌کند.

پس از اینکه دستور action را از طریق اجرای دستی با استفاده از API اعتبارسنجی کردید، اکنون زمان آن رسیده است که ببینید آیا با استفاده از starter که تعریف کرده‌اید نیز اجرا می‌شود یا خیر.

به برگه Devices بروید، دستگاه اکشن و ویژگی (trait) را انتخاب کنید و آن را روی مقدار متفاوتی تنظیم کنید (برای مثال، LevelControl مربوط به light2 (روشنایی) را همانطور که در تصویر زیر نشان داده شده است، روی ۵۰٪ تنظیم کنید):

d0357ec71325d1a8.png

اکنون سعی خواهیم کرد اتوماسیون را با استفاده از دستگاه شروع کننده فعال کنیم. دستگاه شروع کننده‌ای را که هنگام ایجاد اتوماسیون انتخاب کرده‌اید، انتخاب کنید. ویژگی (trait) انتخابی خود را تغییر دهید (برای مثال، OnOff مربوط به starter outlet1 را روی On تنظیم کنید):

۲۳۰c۷۸cd۷۱c۹۵۵۶۴.png

خواهید دید که این کار، اتوماسیون را نیز اجرا می‌کند و ویژگی LevelControl مربوط به دستگاه اکشن light2 را روی مقدار اصلی، یعنی ۱۰۰٪، تنظیم می‌کند:

1f00292128bde1c2.png

تبریک می‌گویم، شما با موفقیت از APIهای Home برای ایجاد اتوماسیون استفاده کردید!

برای کسب اطلاعات بیشتر در مورد API اتوماسیون، به API اتوماسیون اندروید مراجعه کنید.

۵. کشف قابلیت‌ها

رابط‌های برنامه‌نویسی کاربردی خانگی شامل یک رابط برنامه‌نویسی کاربردی اختصاصی به نام رابط برنامه‌نویسی کاربردی اکتشافی (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. کاندیدهای برگردانده شده توسط 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 را فراخوانی کنیم و نتایجی را که در Sample App پشتیبانی می‌کنیم فیلتر کنیم، در مورد چگونگی ادغام این قابلیت در ویرایشگر خود بحث خواهیم کرد.

8a2f0e8940f7056a.png

برای کسب اطلاعات بیشتر در مورد Discovery API، به Leverage device discovery در اندروید مراجعه کنید.

ویرایشگر را ادغام کنید

رایج‌ترین روش برای استفاده از اقدامات کشف‌شده، ارائه آنها به کاربر نهایی برای انتخاب است. درست قبل از اینکه کاربر فیلدهای اتوماسیون پیش‌نویس را انتخاب کند، می‌توانیم لیست اقدامات کشف‌شده را به او نشان دهیم و بسته به مقداری که انتخاب می‌کند، می‌توانیم گره اقدام را در پیش‌نویس اتوماسیون از قبل پر کنیم.

فایل CandidatesView.kt شامل کلاس view است که کاندیدهای کشف‌شده را نمایش می‌دهد. مرحله 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)) }
        }) {
            ...
        }
    }
}

مشابه مرحله ۴.۳ در HomeAppView.kt ، وقتی selectedDraftVM تنظیم می‌شود، DraftView(...) in DraftView.kt` ​​رندر می‌کند:

fun HomeAppView (homeAppVM: HomeAppViewModel) {
   ...
  val selectedDraftVM: DraftViewModel? by homeAppVM.selectedDraftVM.collectAsState()
...
  // If a draft automation is selected, show the draft editor:
  if (selectedDraftVM != null) {
  DraftView(homeAppVM)
  }
   ...
}

دوباره با انتخاب light2 - MOVE_TO_LEVEL که در بخش قبل نشان داده شده است، امتحان کنید، که از شما می‌خواهد بر اساس دستور داوطلب، یک اتوماسیون جدید ایجاد کنید:

15e67763a9241000.png

اکنون که با ایجاد اتوماسیون در برنامه نمونه آشنا شدید، می‌توانید اتوماسیون‌ها را در برنامه‌های خود ادغام کنید.

۶. نمونه‌های اتوماسیون پیشرفته

قبل از جمع‌بندی، چند مثال دیگر از اتوماسیون DSL را بررسی خواهیم کرد. این مثال‌ها برخی از قابلیت‌های پیشرفته‌ای را که می‌توانید با APIها به دست آورید، نشان می‌دهند.

زمان شروع روز

علاوه بر ویژگی‌های دستگاه، APIهای گوگل هوم ویژگی‌های مبتنی بر ساختار مانند Time را نیز ارائه می‌دهند. می‌توانید اتوماسیونی ایجاد کنید که دارای یک شروع‌کننده مبتنی بر زمان باشد، مانند موارد زیر:

automation {
  name = "AutomationName"
  description = "An example automation description."
  isActive = true
  description = "Do ... actions when time is up."
  sequential {
    // starter
    val starter = starter<_>(structure, Time.ScheduledTimeEvent) {
      parameter(
        Time.ScheduledTimeEvent.clockTime(
          LocalTime.of(hour, min, sec, 0)
        )
      )
    }
        // action
  ...
  }
}

دستیار پخش به عنوان اکشن

ویژگی AssistantBroadcast یا به عنوان یک ویژگی در سطح دستگاه در SpeakerDevice (اگر بلندگو از آن پشتیبانی کند) یا به عنوان یک ویژگی در سطح ساختار (زیرا بلندگوهای گوگل و دستگاه‌های تلفن همراه اندروید می‌توانند پخش‌های 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

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()))
      }
    }
  }

حالا که با این قابلیت‌های پیشرفته اتوماسیون آشنا شدید، دست به کار شوید و برنامه‌های فوق‌العاده‌ای بسازید!

۷. تبریک می‌گویم!

تبریک! شما با موفقیت بخش دوم توسعه یک برنامه اندروید با استفاده از API های Google Home را به پایان رساندید. در طول این آزمایشگاه کد، API های اتوماسیون و اکتشاف را بررسی کردید.

امیدواریم از ساخت برنامه‌هایی که به صورت خلاقانه دستگاه‌های موجود در اکوسیستم گوگل هوم را کنترل می‌کنند و سناریوهای اتوماسیون هیجان‌انگیزی را با استفاده از رابط‌های برنامه‌نویسی کاربردی هوم ایجاد می‌کنند، لذت ببرید!

مراحل بعدی

  • برای یادگیری نحوه‌ی اشکال‌زدایی مؤثر برنامه‌ها و عیب‌یابی مشکلات مربوط به APIهای Home، بخش عیب‌یابی را مطالعه کنید.
  • شما می‌توانید با هرگونه توصیه‌ای با ما تماس بگیرید یا هرگونه مشکلی را از طریق ردیاب مشکلات ، موضوع پشتیبانی خانه هوشمند گزارش دهید.