Как создать автоматизацию на устройстве Android

Доступ к Automation API можно получить через Home API для Android, но поскольку точка входа находится в структуре, сначала нужно предоставить разрешение на структуру.

После того как разрешения для структуры будут предоставлены, импортируйте в приложение следующие пакеты:


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

Структура содержит интерфейс HasAutomations со следующими методами автоматизации:

API Описание
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.

Простая автоматизация

Автоматизация, которая поднимает жалюзи в 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. Вызовите atExecutionTime() в Structure или Room. Возвращает 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 можно использовать для дальнейшей настройки вызовов 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, то это условие больше не сможет активировать автоматизацию.