Cómo compilar una automatización en Android

Se puede acceder a las APIs de Automation a través de las APIs de Home para Android, pero, como su punto de entrada es a través de una estructura, primero se debe otorgar permiso en la estructura antes de que se puedan usar.

Una vez que se otorgan los permisos para una estructura, importa estos paquetes a tu app:


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

Una estructura contiene una interfaz HasAutomations con los siguientes métodos específicos de la automatización:

API Descripción
automations() Enumera todas las automatizaciones que pertenecen a la estructura. Solo se devuelven las automatizaciones que creaste a través de las APIs de Home.
createAutomation(automation) Crea una instancia de automatización para una estructura.
deleteAutomation(automationId) Borra una instancia de automatización por su ID.

Crea una automatización

Después de crear una instancia de Home y recibir permisos del usuario, obtén la estructura y los dispositivos:

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

Luego, define la lógica de tu automatización con el DSL de Automation. En las APIs de Home, una automatización se representa con la interfaz Automation. Esta interfaz contiene un conjunto de propiedades:

  • Metadatos, como el nombre y la descripción
  • Son marcas que indican, por ejemplo, si se puede ejecutar la automatización.
  • Es una lista de nodos que contienen la lógica de la automatización, llamada gráfico de automatización, representada por la propiedad automationGraph.

De forma predeterminada, automationGraph es del tipo SequentialFlow, que es una clase que contiene una lista de nodos que se ejecutan en orden secuencial. Cada nodo representa un elemento de la automatización, como un activador, una condición o una acción.

Asigna a la automatización un name y un description.

Cuando se crea una automatización, la marca isActive se establece de forma predeterminada en true, por lo que no es necesario establecer esta marca de forma explícita, a menos que inicialmente desees que la automatización esté inhabilitada. En ese caso, establece la marca en false durante la creación.

La interfaz DraftAutomation se usa para compilar y crear automatizaciones, y la interfaz Automation se usa para la recuperación. Por ejemplo, aquí se muestra el DSL de automatización para una automatización que enciende un dispositivo cuando se enciende otro:

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

Una vez que se define el DSL de automatización, pásalo al método createAutomation() para crear la instancia de DraftAutomation:

val createdAutomation = structure.createAutomation(automation)

Desde aquí, puedes usar todos los demás métodos de automatización en la automatización, como execute(), stop() y update().

Errores de validación

Si la creación de la automatización no pasa la validación, un mensaje de advertencia o error proporciona información sobre el problema. Para obtener más información, consulta la referencia de ValidationIssueType.

Ejemplos de código

Aquí, presentamos un ejemplo de código que se podría usar para implementar partes de las automatizaciones hipotéticas que se describen en la página Diseña una automatización en Android.

Automatización simple

Una automatización que sube las persianas a las 8 a.m. podría implementarse de la siguiente manera:

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

Automatización compleja

Una automatización que activa luces parpadeantes cuando se detecta movimiento podría implementarse de la siguiente manera:

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

Selecciona dispositivos de forma dinámica con filtros de entidades

Cuando escribes una automatización, no estás limitado a especificar dispositivos determinados. Una función llamada filtros de entidades permite que tu automatización seleccione dispositivos en el tiempo de ejecución según varios criterios.

Por ejemplo, con los filtros de entidades, tu automatización podría segmentar lo siguiente:

  • Todos los dispositivos de un tipo de dispositivo en particular
  • Todos los dispositivos de una habitación en particular
  • Todos los dispositivos de un tipo de dispositivo en particular en una habitación específica
  • Todos los dispositivos que están encendidos
  • Todos los dispositivos que están encendidos en una habitación en particular

Para usar filtros de entidades, sigue estos pasos:

  1. En Structure o Room, llama a atExecutionTime(). Esto devuelve un TypedExpression<TypedEntity<StructureType>>.
  2. En este objeto, llama a getDevicesOfType() y pásale un DeviceType.

Los filtros de entidades se pueden usar en activadores, lectores de estado y acciones.

Por ejemplo, para que cualquier luz de encendido/apagado active una automatización desde un activador, haz lo siguiente:

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

Para capturar el estado OnOff de todas las luces de una estructura (específicamente, las luces de encendido/apagado) en un lector de estado, haz lo siguiente:

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

Para obtener las luces de una habitación en particular y usarlas en una condición, haz lo siguiente:

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

En el tiempo de ejecución:

Situación Resultado
Ningún dispositivo cumple con los criterios de un activador. La automatización no se activa.
Ningún dispositivo cumple con los criterios en un lector de estado. La automatización se inicia, pero no hace nada.
Ningún dispositivo cumple con los criterios de una acción. La automatización se inicia, pero la acción no hace nada.

En el siguiente ejemplo, se muestra una automatización que apaga todas las luces, excepto la del pasillo, cada vez que se apaga una luz individual:

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

Ejecuta una automatización

Ejecuta una automatización creada con el método execute():

createdAutomation.execute()

Si la automatización tiene un activador manual, execute() inicia la automatización desde ese punto y omite todos los nodos que preceden al activador manual. Si la automatización no tiene un activador manual, la ejecución comienza desde el nodo que sigue al primer nodo de activador.

Si falla la operación execute(), se puede arrojar una HomeException. Consulta Manejo de errores.

Cómo detener una automatización

Detén una automatización en ejecución con el método stop():


createdAutomation.stop()

Si falla la operación stop(), se puede arrojar una HomeException. Consulta Manejo de errores.

Obtén una lista de las automatizaciones de una estructura

Las automatizaciones se definen a nivel de la estructura. Recopila información sobre la automations() de la estructura para acceder a un Flow de automatizaciones:


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

Como alternativa, asígnale un Collection local:

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

Obtén una automatización por ID

Para obtener una automatización por ID, llama al método automations() en la estructura y haz coincidir el ID:

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

Respuesta:

// 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")
    ]))

Obtén una automatización por su nombre

El método filter() en Kotlin se puede usar para definir mejor las llamadas a la API. Para obtener una automatización por nombre, obtén las automatizaciones de la estructura y filtra por el nombre de la automatización:

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

Obtener todas las automatizaciones de un dispositivo

Para obtener todas las automatizaciones que hacen referencia a un dispositivo determinado, usa el filtrado anidado para analizar el campo automationGraph de cada automatización:

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

Actualiza una automatización

Para actualizar los metadatos de una automatización, llama a su método update() y pásale una expresión lambda que establezca los metadatos:

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

El método update() admite el reemplazo completo de un grafo de automatización, pero no la edición por nodo del grafo. La edición por nodo es propensa a errores debido a las interdependencias entre los nodos. Si deseas cambiar la lógica de una automatización, genera un gráfico nuevo y reemplaza por completo el existente.

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

Cómo borrar una automatización

Para borrar una automatización, usa el método deleteAutomation() de la estructura. Una automatización se debe borrar con su ID.

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)

Si falla la eliminación, es posible que se arroje un HomeException. Consulta Manejo de errores.

Impacto de la eliminación de dispositivos en las automatizaciones

Si un usuario borra un dispositivo que se usa en una automatización, el dispositivo borrado no podrá activar ningún iniciador, y la automatización no podrá leer sus atributos ni enviarle comandos. Por ejemplo, si un usuario borra un OccupancySensorDevice de su casa y una automatización tiene un activador que depende del OccupancySensorDevice, ese activador ya no podrá activar la automatización.