ساختن خودکارسازی در Android

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

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


import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.Id
import com.google.home.Structure

ساختار حاوی یک HasAutomations میانای با روش‌های خاص خودکارسازی زیر است:

میانای برنامه‌سازی کاربردی شرح
automations() همه خودکارسازی‌های متعلق به ساختمان را فهرست کنید. فقط خودکارسازی‌هایی که ازطریق Home API ایجاد کرده‌اید برگردانده می‌شود.
createAutomation(automation) ایجاد نمونه خودکارسازی برای ساختمان.
deleteAutomation(automationId) حذف نمونه خودکارسازی با شناسه آن.

ایجاد کردن خودکارسازی

پس‌از ایجاد نمونه‌ای از Home و دریافت اجازه‌ها از کاربر، ساختار و دستگاه(ها) را دریافت کنید:

val structure = homeManager.structures().list().single()
val device = homeManager.devices().get(Id("myDevice"))!!

سپس منطق خودکارسازی‌تان را بااستفاده از Automation DSL تعریف کنید. در Home API، خودکارسازی با میانای Automation نشان داده می‌شود. این میانای مجموعه‌ای از ویژگی‌ها را دربرمی‌گیرد:

  • فراداده، مانند نام و شرح.
  • پرچم‌هایی که برای مثال نشان می‌دهند آیا خودکارسازی می‌تواند اجرا شود یا نه.
  • فهرستی از گره‌ها که حاوی منطق خودکارسازی هستند و نمودار خودکارسازی نامیده می‌شوند و با ویژگی automationGraph نشان داده می‌شوند.

‫automationGraph به‌طور پیش‌فرض از نوع SequentialFlow است که کلاسی است که حاوی فهرستی از گره‌ها است که به‌ترتیب متوالی اجرا می‌شوند. هر گره نشان‌دهنده عنصری از خودکارسازی است، مثل آغازگر، شرط، یا کنش.

name و description را به خودکارسازی اختصاص دهید.

ایجاد خودکارسازی، پرچم isActive را به‌طور پیش‌فرض روی true تنظیم می‌کند، بنابراین نیازی نیست این پرچم را به‌طور صریح تنظیم کنید، مگراینکه در ابتدا بخواهید خودکارسازی غیرفعال باشد. در این سناریو، پرچم را درطول ایجاد روی false تنظیم کنید.

از میانای DraftAutomation برای ساختن و ایجاد خودکارسازی‌ها استفاده می‌شود، و از میانای Automation برای بازیابی استفاده می‌شود. برای مثال، در اینجا DSL خودکارسازی برای خودکارسازی‌ای که وقتی دستگاه دیگری روشن می‌شود دستگاهی را روشن می‌کند آورده شده است:

import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.Condition
import com.google.home.automation.DraftAutomation
import com.google.home.automation.Equals
import com.google.home.automation.Node
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.matter.standard.OnOff
import com.google.home.Structure

...

val automation: DraftAutomation = automation {
  name = "MyFirstAutomation"
  description = "Turn on a device when another device is turned on."
  sequential {
    val starterNode = starter<_>(device1, OnOffLightDevice, trait=OnOff)
    condition() { expression = stateReaderNode.onOff equals true }
    action(device2, OnOffLightDevice) { command(OnOff.on()) }
  }
}

وقتی DSL خودکارسازی تعریف شد، آن را به روش createAutomation() منتقل کنید تا نمونه DraftAutomation ایجاد شود:

val createdAutomation = structure.createAutomation(automation)

از اینجا می‌توانید از همه روش‌های خودکارسازی دیگر در خودکارسازی استفاده کنید، مثل execute()، stop()، و update().

خطاهای اعتبارسنجی

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

حتی وقتی createAutomation() بدون ایجاد استثنا تکمیل می‌شود، خودکارسازی ایجادشده ممکن است معتبر یا قابل‌اجرا نباشد. زیرینه امکان ذخیره کردن پیش‌نویس‌های نامعتبر خودکارسازی را فراهم می‌کند (برای مثال، اگر کاربر رضایت‌های لازم مثل «تشخیص چهره آشنا» را اعطا نکرده باشد، یا اگر قابلیت‌های دستگاه وجود نداشته باشد).

همیشه باید isValid را درستی‌سنجی کنید و validationIssues را در نمونه Automation برگردانده‌شده بررسی کنید:

val createdAutomation = structure.createAutomation(automation)

if (!createdAutomation.isValid) {
  // Iterate through validation issues to identify errors and warnings
  for (issue in createdAutomation.validationIssues) {
    when (issue.severity) {
      ValidationIssueSeverity.ERROR -> {
        Log.e(
          "Automation",
          "Validation error on node ${issue.node}: ${issue.issueType}"
        )
        // Handle error (for example, prompt the user to enable missing
        // consents or device features)
      }
      ValidationIssueSeverity.WARNING -> {
        Log.w(
          "Automation",
          "Validation warning on node ${issue.node}: ${issue.issueType}"
        )
      }
      else -> {}
    }
  }
}

نمونه‌های کد

در اینجا چند نمونه کد ارائه می‌دهیم که می‌توان از آن‌ها برای پیاده‌سازی بخش‌هایی از خودکارسازی‌های فرضی شرح‌داده‌شده در صفحه طراحی خودکارسازی در Android استفاده کرد.

خودکارسازی ساده

خودکارسازی‌ای که کرکره‌ها را در ساعت ۸:۰۰ صبح بالا می‌برد ممکن است به این صورت اجرا شود:

// get all the automation node candidates in the structure
val allCandidates = structure.allCandidates().first()
// determine whether a scheduled automation can be constructed
val isSchedulingSupported =
  allCandidates.any {
    it is EventCandidate &&
      it.eventFactory == Time.ScheduledTimeEvent &&
      it.unsupportedReasons.isEmpty()
  }
// get the blinds present in the structure
val blinds =
  allCandidates
    .filter {
      it is CommandCandidate &&
        it.commandDescriptor == WindowCoveringTrait.UpOrOpenCommand &&
        it.unsupportedReasons.isEmpty()
    }
    .map { it.entity }
    .filterIsInstance<HomeDevice>()
    .filter { it.has(WindowCoveringDevice) }
 if (isSchedulingSupported && blinds.isNotEmpty()) {
  // Proceed to create automation
  val automation: DraftAutomation = automation {
    name = "Day time open blinds"
    description = "Open all blinds at 8AM everyday"
    isActive = true
    sequential {
      // At 8:00am local time....
      val unused =
        starter(structure, Time.ScheduledTimeEvent) {
          parameter(Time.ScheduledTimeEvent.clockTime(LocalTime.of(8, 0, 0, 0)))
        }
        // ...open all the blinds
       parallel {
        for (blind in blinds) {
          action(blind, WindowCoveringDevice) { command(WindowCovering.upOrOpen()) }
        }
      }
    }
  }
   val createdAutomation = structure.createAutomation(automation)
} else if (!isSchedulingSupported) {
  // Cannot create automation.
  // Set up your address on the structure, then try again.
} else {
  // You don't have any WindowCoveringDevices.
  // Try again after adding some blinds to your structure.
}

خودکارسازی پیچیده

خودکارسازی‌ای که هنگام تشخیص حرکت، چراغ‌های چشمک‌زن را فعال می‌کند ممکن است به این صورت اجرا شود:

import com.google.home.Home
import com.google.home.HomeClient
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure
import com.google.home.automation.action
import com.google.home.automation.automation
import com.google.home.automation.equals
import com.google.home.automation.parallel
import com.google.home.automation.starter
import com.google.home.google.AssistantBroadcast
import com.google.home.matter.standard.OnOff
import com.google.home.matter.standard.OnOff.Companion.toggle
import com.google.home.matter.standard.OnOffLightDevice
import java.time.Duration

// get all the automation node candidates in the structure
val allCandidates = structure.allCandidates().first()

// get the lights present in the structure
val availableLights = allCandidates.filter {
   it is CommandCandidate &&
   it.commandDescriptor == OnOffTrait.OnCommand
}.map { it.entity }
.filterIsInstance<HomeDevice>()
.filter {it.has(OnOffLightDevice) ||
         it.has(ColorTemperatureLightDevice) ||
         it.has(DimmableLightDevice) ||
         it.has(ExtendedColorLightDevice)}

val selectedLights = ... // user selects one or more lights from availableLights

automation {
isActive = true

sequential {
   // If the presence state changes...
   val starterNode = starter<_>(structure, AreaPresenceState)
   // ...and if the area is occupied...
   condition() {
      expression = starterNode.presenceState equals PresenceState.PresenceStateOccupied
   }
   // "blink" the light(s)
   parallel {
            for(light in selectedLights) {
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle())}
         }
      }
   }
}

انتخاب پویای دستگاه‌ها با فیلترهای نهاد

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

برای مثال، بااستفاده از فیلترهای نهاد، خودکارسازی شما می‌تواند موارد زیر را هدف‌یابی کند:

  • همه دستگاه‌های یک نوع دستگاه خاص
  • همه دستگاه‌های موجود در اتاقی خاص
  • همه دستگاه‌های یک نوع دستگاه خاص در یک اتاق خاص
  • همه دستگاه‌هایی که روشن هستند
  • همه دستگاه‌هایی که در اتاق خاصی روشن هستند

برای استفاده از فیلترهای نهاد:

  1. در Structure یا Room، با atExecutionTime() تماس بگیرید. این کار TypedExpression<TypedEntity<StructureType>> را برمی‌گرداند.
  2. در این شیء، getDevicesOfType() را فراخوانی کنید و DeviceType را به آن ارسال کنید.

از فیلترهای نهاد می‌توان در آغازگرها، خواننده‌های وضعیت، و کنش‌ها استفاده کرد.

برای مثال، برای اینکه هر «چراغ روشن/خاموش» خودکارسازی را از آغازگر راه‌اندازی کند:

// If any light is turned on or off
val starter =
  starter(
    entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )

برای ضبط کردن وضعیت OnOff همه چراغ‌های موجود در ساختار (به‌ویژه چراغ‌های روشن/خاموش) در خواننده وضعیت:

// Build a Map<Entity, OnOff>
val onOffStateOfAllLights =
  stateReader(
    entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )

برای دریافت چراغ‌های یک اتاق خاص و استفاده از آن‌ها در یک شرط:

val livingRoomLights =
  stateReader(
    entityExpression = livingRoom.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )
// Are any of the lights in the living room on?
condition { expression = livingRoomLights.values.any { it.onOff equals true } }

در زمان اجرا:

سناریو نتیجه
هیچ دستگاهی با معیارهای آغازگر مطابقت ندارد. خودکارسازی راه‌اندازی نمی‌شود.
هیچ دستگاهی با معیارهای خواننده وضعیت مطابقت ندارد. خودکارسازی شروع می‌شود اما براساس گره شرط ادامه خواهد یافت.
هیچ دستگاهی با معیارهای کنش مطابقت ندارد. خودکارسازی شروع می‌شود اما کنش کاری انجام نمی‌دهد.

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

val unused = automation {
  sequential {
    // If any light is turned on or off
    val starter =
      starter(
        entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
        trait = OnOff,
      )
    condition {
      // Check to see if the triggering light was turned off
      expression = starter.onOff equals false
    }
    // Turn off all lights except the hall light
    action(
      entityExpression =
        structure.atExecutionTime().getDevicesOfType(OnOffLightDevice).filter {
          it notEquals entity(hallwayLight, OnOffLightDevice)
        }
    ) {
      command(OnOff.on())
    }
  }
}

اجرای خودکارسازی

اجرای خودکارسازی ایجادشده بااستفاده از روش execute():

createdAutomation.execute()

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

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

متوقف کردن خودکارسازی

متوقف کردن خودکارسازی درحال اجرا بااستفاده از روش stop():


createdAutomation.stop()

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

دریافت فهرست خودکارسازی‌ها برای ساختمان

خودکارسازی‌ها در سطح ساختمان تعریف می‌شوند. روی automations() ساختمان کلیک کنید تا به Flow خودکارسازی‌ها دسترسی پیدا کنید:


import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
structure.automations().collect {
  println("Available automations:")
  for (automation in it) {
    println(String.format("%S %S", "$automation.id", "$automation.name"))
  }
}

یا آن را به Collection محلی اختصاص دهید:

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

var myAutomations: Collection<Automation> = emptyList()
myAutomations = structure.automations()

دریافت خودکارسازی با شناسه

برای دریافت خودکارسازی با شناسه خودکارسازی، روش automations() را در ساختار فراخوانی کنید و با شناسه مطابقت دهید:

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()

پاسخ:

// Here's how the automation looks like in the get response.
// Here, it's represented as if calling a println(automation.toString())

Automation(
  name = "automation-name",
  description = "automation-description",
  isActive = true,
  id = Id("automation@automation-id"),
  automationGraph = SequentialFlow(
    nodes = [
      Starter(
        entity="device@test-device",
        type="home.matter.0000.types.0101",
        trait="OnOff@6789..."),
      Action(
        entity="device@test-device",
        type="home.matter.0000.types.0101",
        trait="OnOff@8765...",
        command="on")
    ]))

دریافت خودکارسازی با نام

از روش filter() در Kotlin می‌توان برای پالایش بیشتر فراخوانی‌های میانای برنامه‌سازی کاربردی استفاده کرد. برای دریافت خودکارسازی براساس نام، خودکارسازی‌های ساختمان را دریافت کنید و براساس نام خودکارسازی فیلتر کنید:

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().filter {
  it.name.equals("Sunset Blinds") }

دریافت همه خودکارسازی‌های دستگاه

برای دریافت همه خودکارسازی‌هایی که به دستگاه خاصی ارجاع می‌دهند، از فیلتر کردن تودرتو استفاده کنید تا automationGraph هر خودکارسازی را اسکن کنید:

import android.util.Log
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure
import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.Automation.automationGraph
import com.google.home.automation.Node
import com.google.home.automation.ParallelFlow
import com.google.home.automation.SelectFlow
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.automation.StateReader

...

fun collectDescendants(node: Node): List<Node> {
  val d: MutableList<Node> = mutableListOf(node)

  val children: List<Node> =
    when (node) {
      is SequentialFlow -> node.nodes
      is ParallelFlow -> node.nodes
      is SelectFlow -> node.nodes
      else -> emptyList()
    }
  for (c in children) {
    d += collectDescendants(c)
  }
  return d
}

val myDeviceId = "device@452f78ce8-0143-84a-7e32-1d99ab54c83a"
val structure = homeManager.structures().list().single()
val automations =
  structure.automations().first().filter {
    automation: Automation ->
    collectDescendants(automation.automationGraph!!).any { node: Node ->
      when (node) {
        is Starter -> node.entity.id.id == myDeviceId
        is StateReader -> node.entity.id.id == myDeviceId
        is Action -> node.entity.id.id == myDeviceId
        else -> false
      }
    }
  }

به‌روزرسانی خودکارسازی

برای به‌روزرسانی فراداده خودکارسازی، متد update() آن را فراخوانی کنید و عبارت لامبدایی را که فراداده را تنظیم می‌کند به آن ارسال کنید:

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()
automation.update { this.name = "Flashing lights 2" }

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

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: Automation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()
automation.update {
  this.automationGraph = sequential {
    val laundryWasherCompletionEvent =
      starter<_>(laundryWasher, LaundryWasherDevice, OperationCompletionEvent)
    condition {
      expression =
        laundryWasherCompletionEvent.completionErrorCode equals
          // UByte 0x00u means NoError
          0x00u
    }
    action(speaker, SpeakerDevice) { command(AssistantBroadcast.broadcast("laundry is done")) }
    }
  }
}

حذف خودکارسازی

برای حذف خودکارسازی، از روش deleteAutomation() ساختار استفاده کنید. خودکارسازی باید بااستفاده از شناسه آن حذف شود.

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().first()
structure.deleteAutomation(automation.id)

اگر حذف ناموفق باشد، ممکن است HomeException ایجاد شود. رسیدگی به خطا را ببینید.

تأثیر حذف دستگاه بر خودکارسازی‌ها

اگر کاربری دستگاهی را که در خودکارسازی استفاده می‌شود حذف کند، دستگاه حذف‌شده نمی‌تواند هیچ آغازگری را راه‌اندازی کند و خودکارسازی نمی‌تواند مشخصه‌های آن را بخواند یا به آن فرمان دهد. برای مثال، اگر کاربری OccupancySensorDevice را از خانه‌اش حذف کند و خودکارسازی‌ای آغازگری داشته باشد که به OccupancySensorDevice وابسته باشد، آن آغازگر دیگر نمی‌تواند خودکارسازی را فعال کند.