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

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

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


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

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

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

אחרי שמגדירים את ה-DSL של האוטומציה, מעבירים אותו לשיטה createAutomation() כדי ליצור את המופע של DraftAutomation:

val createdAutomation = structure.createAutomation(automation)

מכאן אפשר להשתמש בכל שאר שיטות האוטומציה באוטומציה, כמו execute(),‏ stop() ו-update().

שגיאות אימות

אם תהליך היצירה של האוטומציה לא עובר אימות, תוצג הודעת אזהרה או שגיאה עם מידע על הבעיה. למידע נוסף, עיינו במקור המידע ValidationIssueType.

דוגמאות קוד

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

אוטומציה פשוטה

פעולה אוטומטית להרמת התריסים בשעה 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())}
         }
      }
   }
}

אוטומציה גנרית

הקוד הבא מראה איך אפשר לאסוף נתונים על הבית של המשתמש באמצעות Structure API,‏ Device API ו-Discovery API כדי להטמיע אפליקציה שמנחה את המשתמש ליצור אוטומציה גנרית משלו (כפי שמתואר בקטע תכנון אוטומציה: אוטומציה גנרית).

val structureFlow: Flow<Structure> = home.structures().itemFlow(myStructureId)

// Get a snapshot of the structure.
val structure: Structure = structureFlow.first()

   // map of devices where key is device id
val devicesById = mutableMapOf<Id,HomeDevice>()

home.devices().list().map { devicesById.put(it.id, it) }

// map of rooms where key is room id
val roomsByName: MutableMap<String,Room?> = mutableMapOf<String,Room?>()

structure.rooms().list().map { roomsByName.put(it.name, it) }

// map of commands where key is trait id
val commandsByTrait: MutableMap<String,MutableList<CommandCandidate>?> = mutableMapOf<String,MutableList<CommandCandidate>?>()

// map of commands where key is DeviceType.Metadata.hashcode()
val commandsByDeviceType: MutableMap<Id,MutableList<CommandCandidate>?> = mutableMapOf<Id,MutableList<CommandCandidate>?>()

// map of commands where key is entity id
val commandsByEntity: MutableMap<Id,MutableList<CommandCandidate>?> = mutableMapOf<Id,MutableList<CommandCandidate>?>()


// map of stateReaders where key is trait id
val stateReadersByTrait: MutableMap<Int,MutableList<StateReaderCandidate>?> = mutableMapOf<Int,MutableList<StateReaderCandidate>?>()

// map of stateReaders where key is DeviceType.Metadata.hashcode()
val stateReadersByDeviceType: MutableMap<Int,MutableList<StateReaderCandidate>?> = mutableMapOf<Int,MutableList<StateReaderCandidate>?>()

// map of stateReaders where key is  entity id
val stateReadersByEntity: MutableMap<Int,MutableList<StateReaderCandidate>?> = mutableMapOf<Int,MutableList<StateReaderCandidate>?>()

// map of starters where key is trait id
val startersByTrait: MutableMap<Int,MutableList<StarterCandidate>?> = mutableMapOf<Int,MutableList<StarterCandidate>?>()

// map of starters where key is DeviceType.Metadata.hashcode()
val startersByDeviceType: MutableMap<Int,MutableList<CommandCandidate>?> = mutableMapOf<Int,MutableList<CommandCandidate>?>()

// map of starters where key is entity id
val startersByEntity: MutableMap<Int,MutableList<StarterCandidate>?> = mutableMapOf<Int,MutableList<StarterCandidate>?>()

// populate candidate maps
structure.allCandidates().first().map {
  when (it) {
   is CommandCandidate -> {

      // update commandsByTrait
      // TODO refactor into generic function to eliminate duplicate code
      var commandsByTraitList: MutableList<CommandCandidate>? = commandsByTrait.get(it.trait.factory.traitId: String)
      if(commandsByTraitList == null) { commandsByTraitList = arrayListOf<CommandCandidate>() }

      commandsByTraitList.add(it)
      commandsByTrait.put(it.trait.factory.traitId, commandsByTraitList)

      // update commandsByDeviceType
      // TODO refactor into generic function to eliminate duplicate code
      for (t in it.types) {

      //TODO filter out device types not present in the home

      //TODO how to get a reference to device type id? 

      var commandsByDeviceTypeList: MutableList<CommandCandidate>? = commandsByDeviceType.get(t.factory.typeId: Id)

      if (commandsByDeviceTypeList == null)  { commandsByDeviceTypeList = arrayListOf<CommandCandidate>() }

      commandsByDeviceTypeList.add(it)

      commandsByDeviceType.put(t.factory.typeId, commandsByDeviceTypeList)
      }

      // update commandsByEntity
      // TODO refactor into generic function to eliminate duplicate code
     var commandsByEntityList: MutableList<CommandCandidate>? = commandsByEntity.get(it.entity.id: Id)
      if ( commandsByEntityList == null ) {commandsByEntityList = arrayListOf<CommandCandidate>()}
      commandsByEntityList.add(it)
      commandsByEntity.put(it.entity.id, commandsByEntityList)
   }

/*
   is StateReaderCandidate -> {

      // update stateReadersByTrait

      var stateReadersList: MutableList<StateReaderCandidate>? = stateReadersByTrait.get(it.trait.factory.traitId)
      if(stateReadersList == null) { stateReadersList = arrayListOf<StateReaderCandidate>() }

      stateReadersList.add(it)
      stateReadersByTrait.put(it.trait.factory.traitId, stateReadersList)

   // update stateReadersByDeviceType

      for (t in it.types) {
      //TODO filter out device types not present in the home
      var stateReadersList: MutableList<StateReaderCandidate>? = stateReadersByDeviceType.get(t.metadata.hashcode())

      if (stateReadersList == null)  { stateReadersList = arrayListOf<StateReaderCandidate>() }

      stateReadersList.put(it)

      stateReadersByDeviceType.put(t.metadata.hashCode(),deviceTypeStateReaderList)
      }

      // update stateReadersByEntity

      MutableList<StateReaderCandidate> entityStateReaderList? = stateReadersByEntity.get(it.entity.id)
      if  entityStateReaderList  is null  {entityStateReaderList = arrayListOf<StateReaderCandidate>()}
      entityStateReaderList.add(it)
      stateReadersByEntity.put(it.entity.id, entityStateReaderList)
   }

   */

   /*
   is StarterCandidate  -> {

      // update startersByTrait

      var startersList: MutableList<StateReaderCandidate>? = startersByTrait.get(it.trait.factory.traitId)
      if(startersList == null) { startersList = arrayListOf<StateReaderCandidate>() }

      startersList.add(it)
      startersByTrait.put(it.trait.factory.traitId, startersList)

   // update startersByDeviceType

      for (t in it.types) {
      //TODO filter out device types not present in the home
      var startersList: MutableList<StateReaderCandidate>? = startersByDeviceType.get(t.metadata.hashcode())

      if (startersList == null)  { startersList = arrayListOf<StateReaderCandidate>() }

      startersList.put(it)

      startersByDeviceType.put(t.metadata.hashCode(),deviceTypeStateReaderList)
      }

      // update startersByEntity

      MutableList<StateReaderCandidate> entityStateReaderList? = startersByEntity.get(it.entity.id)
      if  entityStateReaderList  is null  {entityStateReaderList = arrayListOf<StateReaderCandidate>()}
      entityStateReaderList.add(it)
      stateReadersByEntity.put(it.entity.id, entityStateReaderList)

      }
*/
 else -> println("unknown type encountered: " + it)

  }
}

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

מריצים פעולה אוטומטית שנוצרה באמצעות השיטה 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()

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

כדי לקבל אוטומציה לפי מזהה האוטומציה, צריך לבצע קריאה ל-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() שלו ולהעביר לה ביטוי lambda שמגדיר את המטא-נתונים:

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

מחיקת פעולה אוטומטית

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