יצירת פעולה אוטומטית ב-Android

אפשר לגשת אל Automation APIs דרך Home APIs ל-Android, אבל נקודת הכניסה שלהם היא דרך מבנה, ולכן צריך קודם לתת הרשאה למבנה לפני שאפשר להשתמש בהם.

אחרי שמעניקים הרשאות למבנה, מייבאים את החבילות האלה לאפליקציה:


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

מבנה מכיל את הממשק HasAutomations עם ה-methods הבאות שספציפיות לאוטומציה:

API תיאור
automations() רשימה של כל האוטומציות ששייכות למבנה. מוחזרות רק אוטומציות שיצרתם באמצעות ממשקי ה-API של Home.
createAutomation(automation) יצירת מופע של פעולה אוטומטית למבנה.
deleteAutomation(automationId) מחיקת מופע של פעולה אוטומטית לפי המזהה שלו.

יצירת פעולה אוטומטית

אחרי שיוצרים מופע של Home ומקבלים הרשאות מהמשתמש, מקבלים את המבנה ואת המכשירים:

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

לאחר מכן מגדירים את הלוגיקה של האוטומציה באמצעות Automation DSL. בממשקי ה-API של Home, אוטומציה מיוצגת על ידי הממשק 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()) }
  }
}

אחרי שמגדירים את שפת התחום לאוטומציה, מעבירים אותה אל ה-method‏ 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.

פעולות אוטומטיות פשוטות

אוטומציה להרמת התריסים בשעה 8:00 בבוקר עשויה להיות מיושמת כך:

// 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 } }

בזמן הריצה:

תרחיש תוצאה
אף מכשיר לא עומד בקריטריונים בחבילת Starter. האוטומציה לא מופעלת.
אף מכשיר לא עומד בקריטריונים בקורא מצב. הפעולה האוטומטית מתחילה אבל תמשיך לפעול על סמך צומת התנאי.
אף מכשיר לא עומד בקריטריונים של פעולה. האוטומציה מתחילה אבל הפעולה לא עושה כלום.

בדוגמה הבאה מוצגת אוטומציה שמכבה את כל האורות חוץ מאור המסדרון בכל פעם שמכבים אור מסוים:

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. למידע נוסף על טיפול בשגיאות

קבלת רשימה של פעולות אוטומטיות במבנה

הפעולות האוטומטיות מוגדרות ברמת המבנה. כדי לגשת לFlow של פעולות אוטומטיות, צריך לאסוף נתונים על 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()
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()

אחזור אוטומציה לפי מזהה

כדי לקבל אוטומציה לפי מזהה האוטומציה, מבצעים קריאה ל-method‏ 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")
    ]))

קבלת פעולה אוטומטית לפי שם

אפשר להשתמש ב-method‏ filter() ב-Kotlin כדי לחדד עוד יותר את הקריאות ל-API. כדי לקבל פעולה אוטומטית לפי שם, מקבלים את הפעולות האוטומטיות של המבנה ומסננים לפי שם הפעולה האוטומטית:

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, הטריגר לפעולה הזה כבר לא יכול להפעיל את הפעולות האוטומטיות.