ניצול גילוי המכשירים

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

Discovery API מתעלם ממכשירים או מתכונות שנמצאים במבנה שלא נתמך ב-Automation API, וגם ממכשירים או מתכונות שלא נרשמו ב-FactoryRegistry. למידע נוסף על השימוש ב-FactoryRegistry, ראו יצירת מכונה של Home.

שימוש ב-API

הליבה של Discovery API היא הממשק HasCandidates, שהוא הבסיס של היררכיית הטיפוסים שכוללת את Structure,‏ Room ו-HomeDevice.

בממשק HasCandidates מוגדרות שתי שיטות, candidates() ו-allCandidates(), ששתיהן מחזירות אובייקטים מסוג Flow.

  • הפקודה candidates() יוצרת רשימה של מועמדים לאוטומציה של הישות (Structure, Room, HomeDevice).

  • הפונקציה allCandidates() יוצרת רשימה של מועמדים לאוטומציה עבור הישות וכל הצאצאים שלה. אין תמיכה בשיטה הזו ב-Room.

בניגוד לאובייקטים האחרים מסוג Flow שמוחזרים על ידי ממשקי ה-API של Home, אובייקטים אלה מכילים קובץ snapshot חד-פעמי.

כדי לקבל את רשימת המועמדים העדכנית ביותר, המפתח צריך לקרוא ל-candidates() או ל-allCandidates() בכל פעם, ולא יכול פשוט לקרוא ל-collect() באובייקטים Flow. בנוסף, מאחר ששתי השיטות האלה צורכות הרבה משאבים, קריאה אליהן בתדירות גבוהה יותר מדקה אחת תוביל להחזרת נתונים שנשמרו במטמון, שעשויים שלא לשקף את המצב הנוכחי בפועל באותו רגע.

הממשק NodeCandidate מייצג צומת מתאים שנמצא בשתי השיטות האלה, והוא שורש היררכיה שכוללת את הממשקים הבאים:

והמחלקות הבאות:

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

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

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

import com.google.home.Structure
import com.google.home.automation.CommandCandidate
import com.google.home.automation.EventCandidate
import com.google.home.automation.Automation
import com.google.home.automation.DraftAutomation
import com.google.home.platform.Time
import java.time.LocalTime
import com.google.home.matter.standard.WindowCoveringTrait
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

fun createAutomationWithDiscoveryApiTimeStarter(
  structureName: String,
  scheduledTimeInSecond: Int,
): String = runBlocking {

  // get the Structure
  val structure = homeClient.structures().findStructureByName(structureName)

  // get an event candidate
  val clockTimeStarter =
    structure
      .allCandidates().first().firstOrNull {candidate ->
        candidate is EventCandidate && candidate.eventFactory == Time.ScheduledTimeEvent
      } as EventCandidate

  // retrieve the first 'DownOrClose' command encountered
  val downOrCloseCommand =
    structure.allCandidates().first().firstOrNull {
        candidate ->
      candidate is CommandCandidate
        && candidate.commandDescriptor == WindowCoveringTrait.DownOrCloseCommand
    } as CommandCandidate

  val blinds = ...

  // prompt user to select the WindowCoveringDevice
  ...

if (clockTimeStarter && downOrCloseCommand && blinds) {
  // Create the draft automation
  val draftAutomation: DraftAutomation = automation {
    name = ""
    description = ""
    isActive = true
    sequential {
      val mainStarter = starter<_>(structure, Time.ScheduledTimeEvent) {
          parameter(
            Time.ScheduledTimeEvent.clockTime(
              LocalTime.ofSecondOfDay(scheduledTimeInSecond.toLong())
            )
          )
        }
      action(blinds, WindowCoveringDevice) { command(WindowCoveringTrait.downOrClose())
    }
  }

  // Create the automation in the structure
  val automation = structure.createAutomation(draftAutomation)
  return@runBlocking automation.id.id
} else  ... //the automation cannot be created

בדוגמה הבאה נוצרת פעולה אוטומטית להגדרת רמת הבהירות של תאורה כשהיא מופעלת.

import com.google.home.Structure
import com.google.home.automation.CommandCandidate
import com.google.home.automation.TraitAttributesCandidate
import com.google.home.automation.Automation
import com.google.home.automation.DraftAutomation
import com.google.home.matter.standard.LevelControl
import com.google.home.matter.standard.LevelControlTrait.MoveToLevelCommand
import com.google.home.matter.standard.OnOff
import com.google.home.matter.standard.OnOff.Companion.onOff
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import com.google.home.automation.equals

fun createAutomationWithDiscoveryApiDimLight(
  structureName: String,
): String = runBlocking {

  // get the Structure
  val structure: Structure = homeClient.structures().findStructureByName(structureName)

  /**
   * When I turn on the light, move the brightness level to 55
   */
  val allCandidates = structure.allCandidates().first()
  val dimmableLightDevice = structure.devices().list().first {it.has(OnOff) && it.has(LevelControl)}
  val starterCandidate =
    allCandidates
      .filterIsInstance<TraitAttributesCandidate>()
      .first { it.entity == dimmableLightDevice && it.trait == OnOff }

  val actionCandidate =
    allCandidates
      .filterIsInstance<CommandCandidate>()
      .first {it.entity == dimmableLightDevice && it.commandDescriptor == MoveToLevelCommand }

  if (starterCandidate && actionCandidate) {
    // Create the draft automation
    val draftAutomation: DraftAutomation = automation {
      sequential {
        val starter =  starter<_>(dimmableLightDevice, OnOffLightDevice, OnOff)
        condition { expression = starter.onOff equals true }
        action(dimmableLightDevice,DimmableLightDevice) {
          mapOf(MoveToLevelCommand.Request.CommandFields.level to 55u.toUByte())
        )
      }
    }

    // Create the automation in the structure
    val automation = structure.createAutomation(draftAutomation)
    return@runBlocking automation.id.id
    }
} else ... //the automation cannot be created

בדיקת התנאים המוקדמים

באמצעות Discovery API תוכלו לדעת אם חסרים למאפיין תנאים מוקדמים לשימוש, כמו מינוי או כתובת של מבנה. הוא עושה זאת באמצעות המאפיין unsupportedReasons של הכיתה Candidate. המאפיין הזה מאוכלס ב-UnsupportedCandidateReason במהלך הקריאה ל-candidates(). אותו מידע מופיע בהודעות השגיאה של האימות כשמפעילים את createAutomation().

דוגמאות לסיבות:

  • MissingStructureAddressSetup מאפשר למשתמש לדעת שצריך להגדיר כתובת כדי להשתמש במאפיין Time. במאמר שינוי כתובת הבית ב-Google מוסבר איך משתמשים יכולים להזין את כתובת המבנה באמצעות Google Home app (GHA).
  • MissingPresenceSensingSetup מאפשרת למשתמש לדעת שצריך להגדיר את המצב כדי להשתמש במאפיינים AreaPresenceState ו-AreaAttendanceState.
  • MissingSubscription מאפשר למשתמש לדעת שנדרש מינוי ל-Nest Aware כדי להשתמש במאפיין ObjectDetection.

לדוגמה, כדי לטפל ב-MissingStructureAddressSetup UnsupportedCandidateReason, כדאי להציג הודעה קופצת באפליקציה ולפתוח את GHA כדי לאפשר למשתמש לספק את הכתובת של המבנה:

val structure = homeManager.structures().list().single()
val allCandidates = structure.allCandidates().list().single()
val scheduledStarterCandidate = allCandidates.first { it is EventCandidate && it.eventFactory == ScheduledTimeEvent }
if (scheduledStarterCandidate.unsupportedReasons.any { it is MissingStructureAddressSetup }) {
  showToast("No Structure Address setup. Redirecting to GHA to set up an address.")
  launchChangeAddress(...)
}

אימות הפרמטרים

‏Discovery API מחזיר את הערכים שמותר להזין למאפיין, לפרמטר או לשדה אירוע, בצורת מופע של Constraint. המידע הזה מאפשר למפתח האפליקציה למנוע ממשתמשים להגדיר ערכים לא חוקיים.

לכל תת-הסוגים של Constraint יש דרך משלהם לייצג ערכים קבילים.

טבלה: סוגי Constraint
מחלקת אילוצים מאפיינים שמייצגים ערכים קבילים
BitmapConstraint combinedBits
BooleanConstraint
ByteConstraint maxLength וגם minLength
EnumConstraint allowedSet
NumberRangeConstraint lowerBound, upperBound, step וגם unit
NumberSetConstraint allowedSet וגם unit
StringConstraint allowedSet, disallowedSet, isCaseSensitive, maxLength, minLength וגם regex
StructConstraint fieldConstraints
ListConstraint elementConstraint

שימוש במגבלות

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


import android.content.Context
import com.google.home.Home
import com.google.home.Structure
import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.CommandCandidate
import com.google.home.automation.Condition
import com.google.home.automation.Constraint
import com.google.home.automation.Equals
import com.google.home.automation.EventCandidate
import com.google.home.automation.HasCandidates
import com.google.home.automation.Node
import com.google.home.automation.NodeCandidate
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.matter.standard.LevelControlTrait

// Filter the output of candidates() to find the TraitAttributesCandidate
// for the LevelControl trait.

val levelCommand =
        structure
          .allCandidates()
          .first()
          .firstOrNull { candidate ->
            candidate is CommandCandidate && candidate.command == LevelControlTrait.MoveToLevelCommand
 } as? CommandCandidate

var levelConstraint = null

// Get the NodeCandidate instance's fieldDetailsMap and
// retrieve the Constraint associated with the level parameter.
// In this case, it is a NumberRangeConstraint.
if (levelCommand != null) {
    levelConstraint =
      levelCommand.fieldDetailsMap[
        LevelControlTrait.MoveToLevelCommand.Request.CommandFields.level
        ]!!.constraint
}

...

// Test the value against the Constraint (ignoring step and unit)
if ( value in levelConstraint.lowerBound..levelConstraint.upperBound) {
   // ok to use the value
}

השוואה בין Device API לבין Discovery API

אפשר לגלות את סוגי המכשירים, המאפיינים והמאפיינים שלהם בלי להשתמש ב-Discovery API. בעזרת Device API אפשר לגלות:

  1. סוגי המכשירים הראשיים שהמשתמש העניק למפתח הרשאה לשלוט בהם, באמצעות השיטה DeviceType.Metadata.isPrimaryType().
  2. האם כל מכשיר תומך בכל המאפיינים הנדרשים לאוטומציה, באמצעות השיטה HasTraits.has().
  3. האם כל מאפיין תומך בכל המאפיינים והפקודות הנדרשים לצורך האוטומציה, באמצעות השיטה supports().

חשוב לציין שאם משתמשים ב-Device API כדי לבצע זיהוי, לא תוכלו ליהנות מהיכולות הבאות של Discovery API:

  • סינון אוטומטי של מאפיינים שלא נתמכים ב-Automation API.
  • היכולת לספק למשתמשים אפשרות לבחור ערך חוקי למאפיינים ולפרמטרים שמשתמשים באילוצים.