1. Antes de comenzar
Este es el segundo codelab de la serie sobre la creación de una app para Android con las APIs de Google Home. En este codelab, te explicaremos cómo crear automatizaciones para la casa y te brindaremos algunas sugerencias sobre las prácticas recomendadas para usar las APIs. Si aún no completaste el primer codelab, Crea una app para dispositivos móviles con las APIs de Home en Android, te recomendamos que lo hagas antes de comenzar este.
Las APIs de Google Home proporcionan un conjunto de bibliotecas para que los desarrolladores de Android controlen los dispositivos inteligentes para la casa dentro del ecosistema de Google Home. Con estas nuevas APIs, los desarrolladores podrán establecer automatizaciones para una casa inteligente que puedan controlar las capacidades de los dispositivos según condiciones predefinidas. Google también proporciona una API de Discovery que te permite consultar dispositivos para averiguar qué atributos y comandos admiten.
Requisitos previos
- Completar el codelab Cómo compilar una app para dispositivos móviles con las APIs de Home en Android
- Conocimiento del ecosistema de Google Home (de nube a nube y Matter)
- Una estación de trabajo con Android Studio (2024.3.1 Ladybug o versiones posteriores) instalado
- Un teléfono Android que cumpla con los requisitos de las APIs de Home (consulta Requisitos previos), con los Servicios de Google Play y la app de Google Home instalados Un emulador no funcionará. Solo se admiten teléfonos Android físicos para la app de ejemplo.
- Un Google Home Hub compatible que admita las APIs de Google Home.
- Opcional: Un dispositivo inteligente para la casa compatible con las APIs de Google Home
Qué aprenderás
- Cómo crear automatizaciones para dispositivos inteligentes para la casa con las APIs de Home
- Cómo usar las APIs de Discovery para explorar las capacidades admitidas de los dispositivos
- Cómo emplear las prácticas recomendadas cuando compilas tus apps con las APIs de Home
2. Configurando el proyecto
En el siguiente diagrama, se ilustra la arquitectura de una app de las APIs de Home:
- Código de la app: Es el código principal en el que trabajan los desarrolladores para compilar la interfaz de usuario de la app y la lógica para interactuar con el SDK de las APIs de Home.
- SDK de Home APIs: El SDK de Home APIs que proporciona Google funciona con el servicio de Home APIs en GMSCore para controlar dispositivos para la casa inteligente. Los desarrolladores compilan apps que funcionan con las APIs de Home incluyéndolas en el SDK de las APIs de Home.
- GMSCore en Android: GMSCore, también conocido como Servicios de Google Play, es una plataforma de Google que proporciona servicios del sistema principales y habilita funciones clave en todos los dispositivos Android certificados. El módulo principal de los Servicios de Google Play contiene los servicios que interactúan con las APIs de Home.
En este codelab, profundizaremos en lo que vimos en Cómo compilar una app para dispositivos móviles con las APIs de Home en Android.
Asegúrate de tener una estructura con al menos dos dispositivos compatibles configurados y en funcionamiento en la cuenta. Como configuraremos automatizaciones en este codelab (un cambio en el estado de un dispositivo activa una acción en otro), necesitarás dos dispositivos para ver los resultados.
Obtén la app de ejemplo
El código fuente de la app de ejemplo está disponible en GitHub en el repositorio google-home/google-home-api-sample-app-android.
En este codelab, se usan los ejemplos de la rama codelab-branch-2
de la app de ejemplo.
Navega hasta donde quieras guardar el proyecto y clona la rama codelab-branch-2
:
$ git clone -b codelab-branch-2 https://github.com/google-home/google-home-api-sample-app-android.git
Ten en cuenta que esta es una rama diferente a la que se usa en Cómo compilar una app para dispositivos móviles con las APIs de Home en Android. Esta rama de la base de código se basa en el punto en el que terminó el primer codelab. Esta vez, los ejemplos te guiarán para crear automatizaciones. Si completaste el codelab anterior y pudiste hacer que funcionaran todas las funciones, puedes usar el mismo proyecto de Android Studio para completar este codelab en lugar de usar codelab-branch-2
.
Una vez que tengas el código fuente compilado y listo para ejecutarse en tu dispositivo móvil, continúa con la siguiente sección.
3. Más información sobre las automatizaciones
Las automatizaciones son un conjunto de instrucciones "si sucede esto, entonces haz esto otro" que pueden controlar los estados de los dispositivos de forma automática según los factores seleccionados. Los desarrolladores pueden usar automatizaciones para crear funciones interactivas avanzadas en sus APIs.
Las automatizaciones se componen de tres tipos diferentes de componentes conocidos como nodos: activadores, acciones y condiciones. Estos nodos trabajan en conjunto para automatizar comportamientos con dispositivos inteligentes para la casa. Por lo general, se evalúan en el siguiente orden:
- Activador: Define las condiciones iniciales que activan la automatización, como un cambio en el valor de un rasgo. Una automatización debe tener un activador.
- Condición: Son las restricciones adicionales que se deben evaluar después de que se activa una automatización. La expresión de una Condition debe evaluarse como verdadera para que se ejecuten las acciones de una automatización.
- Acción: Son los comandos o las actualizaciones de estado que se realizan cuando se cumplen todas las condiciones.
Por ejemplo, puedes tener una automatización que atenúe las luces de una habitación cuando se active un interruptor, mientras la TV de esa habitación está encendida. En este ejemplo:
- Starter: El interruptor de la habitación está activado.
- Condición: El estado de encendido y apagado de la TV se evalúa como Encendido.
- Acción: Las luces de la misma habitación que el interruptor se atenúan.
El motor de automatización evalúa estos nodos de forma serial o paralela.
Un flujo secuencial contiene nodos que se ejecutan en orden secuencial. Por lo general, estos serían el activador, la condición y la acción.
Un flujo paralelo puede tener varios nodos de acción que se ejecutan de forma simultánea, como encender varias luces al mismo tiempo. Los nodos que siguen un flujo paralelo no se ejecutarán hasta que finalicen todas las ramas del flujo paralelo.
Existen otros tipos de nodos en el esquema de automatización. Puedes obtener más información sobre ellos en la sección Nodos de la Guía para desarrolladores de las APIs de Home. Además, los desarrolladores pueden combinar diferentes tipos de nodos para crear automatizaciones complejas, como las siguientes:
Los desarrolladores proporcionan estos nodos al motor de automatización con un lenguaje específico del dominio (DSL) creado específicamente para las automatizaciones de Google Home.
Explora el DSL de Automation
Un lenguaje específico del dominio (DSL) es un lenguaje que se usa para capturar el comportamiento del sistema en el código. El compilador genera clases de datos que se serializan en JSON de búfer de protocolo y se usan para realizar llamadas a los servicios de automatización de Google.
El DSL busca el siguiente esquema:
automation {
name = "AutomationName"
description = "An example automation description."
isActive = true
sequential {
val onOffTrait = starter<_>(device1, OnOffLightDevice, OnOff)
condition() { expression = onOffTrait.onOff equals true }
action(device2, OnOffLightDevice) { command(OnOff.on()) }
}
}
La automatización del ejemplo anterior sincroniza dos bombillas. Cuando el estado OnOff
de device1
cambia a On
(onOffTrait.onOff equals true
), el estado OnOff
de device2
cambia a On
(command(OnOff.on()
).
Cuando trabajes con automatizaciones, ten en cuenta que existen límites de recursos.
Las automatizaciones son una herramienta muy útil para crear capacidades automatizadas en una casa inteligente. En el caso de uso más básico, puedes codificar de forma explícita una automatización para que use dispositivos y rasgos específicos. Sin embargo, un caso de uso más práctico es aquel en el que la app permite que el usuario configure los dispositivos, los comandos y los parámetros de una automatización. En la siguiente sección, se explica cómo crear un editor de automatización que permita al usuario hacer exactamente eso.
4. Compila un editor de automatizaciones
En la app de ejemplo, crearemos un editor de automatizaciones con el que los usuarios podrán seleccionar dispositivos, las capacidades (acciones) que quieren usar y cómo se activan las automatizaciones con activadores.
Configura activadores
El activador de automatización es el punto de entrada para la automatización. Un activador inicia una automatización cuando ocurre un evento determinado. En la app de ejemplo, capturamos los iniciadores de automatización con la clase StarterViewModel
, que se encuentra en el archivo fuente StarterViewModel.kt
, y mostramos la vista del editor con StarterView
(StarterView.kt
).
Un nodo inicial necesita los siguientes elementos:
- Dispositivo
- Rasgo
- Operación
- Valor
El dispositivo y el rasgo se pueden seleccionar de los objetos que devuelve la API de Devices. Los comandos y parámetros de cada dispositivo compatible son un asunto más complejo que debe tratarse por separado.
La app define una lista preestablecida de operaciones:
// List of operations available when creating automation starters:
enum class Operation {
EQUALS,
NOT_EQUALS,
GREATER_THAN,
GREATER_THAN_OR_EQUALS,
LESS_THAN,
LESS_THAN_OR_EQUALS
}
Luego, para cada rasgo admitido, realiza un seguimiento de las operaciones admitidas:
// List of operations available when comparing booleans:
object BooleanOperations : Operations(listOf(
Operation.EQUALS,
Operation.NOT_EQUALS
))
// List of operations available when comparing values:
object LevelOperations : Operations(listOf(
Operation.GREATER_THAN,
Operation.GREATER_THAN_OR_EQUALS,
Operation.LESS_THAN,
Operation.LESS_THAN_OR_EQUALS
))
De manera similar, la app de ejemplo hace un seguimiento de los valores que se pueden asignar a los rasgos:
enum class OnOffValue {
On,
Off,
}
enum class ThermostatValue {
Heat,
Cool,
Off,
}
Y realiza un seguimiento de una asignación entre los valores definidos por la app y los definidos por las APIs:
val valuesOnOff: Map<OnOffValue, Boolean> = mapOf(
OnOffValue.On to true,
OnOffValue.Off to false,
)
val valuesThermostat: Map<ThermostatValue, ThermostatTrait.SystemModeEnum> = mapOf(
ThermostatValue.Heat to ThermostatTrait.SystemModeEnum.Heat,
ThermostatValue.Cool to ThermostatTrait.SystemModeEnum.Cool,
ThermostatValue.Off to ThermostatTrait.SystemModeEnum.Off,
)
Luego, la app muestra un conjunto de elementos de vista que los usuarios pueden usar para seleccionar los campos obligatorios.
Quita el comentario del paso 4.1.1 en el archivo StarterView.kt
para renderizar todos los dispositivos iniciales y, luego, implementa la devolución de llamada de clic en un DropdownMenu
:
val deviceVMs: List<DeviceViewModel> = structureVM.deviceVMs.collectAsState().value
...
DropdownMenu(expanded = expandedDeviceSelection, onDismissRequest = { expandedDeviceSelection = false }) {
// TODO: 4.1.1 - Starter device selection dropdown
// for (deviceVM in deviceVMs) {
// DropdownMenuItem(
// text = { Text(deviceVM.name) },
// onClick = {
// scope.launch {
// starterDeviceVM.value = deviceVM
// starterType.value = deviceVM.type.value
// starterTrait.value = null
// starterOperation.value = null
// }
// expandedDeviceSelection = false
// }
// )
// }
}
Quita el comentario del paso 4.1.2 en el archivo StarterView.kt
para renderizar todos los rasgos del dispositivo de inicio y, luego, implementa la devolución de llamada de clic en un DropdownMenu
:
// Selected starter attributes for StarterView on screen:
val starterDeviceVM: MutableState<DeviceViewModel?> = remember {
mutableStateOf(starterVM.deviceVM.value) }
...
DropdownMenu(expanded = expandedTraitSelection, onDismissRequest = { expandedTraitSelection = false }) {
// TODO: 4.1.2 - Starter device traits selection dropdown
// val deviceTraits = starterDeviceVM.value?.traits?.collectAsState()?.value!!
// for (trait in deviceTraits) {
// DropdownMenuItem(
// text = { Text(trait.factory.toString()) },
// onClick = {
// scope.launch {
// starterTrait.value = trait.factory
// starterOperation.value = null
// }
// expandedTraitSelection = false
// }
// )
}
}
Quita la marca de comentario del paso 4.1.3 en el archivo StarterView.kt
para renderizar todas las operaciones del rasgo seleccionado y, luego, implementa la devolución de llamada de clic en un DropdownMenu
:
val starterOperation: MutableState<StarterViewModel.Operation?> = remember {
mutableStateOf(starterVM.operation.value) }
...
DropdownMenu(expanded = expandedOperationSelection, onDismissRequest = { expandedOperationSelection = false }) {
// ...
if (!StarterViewModel.starterOperations.containsKey(starterTrait.value))
return@DropdownMenu
// TODO: 4.1.3 - Starter device trait operations selection dropdown
// val operations: List<StarterViewModel.Operation> = StarterViewModel.starterOperations.get(starterTrait.value ?: OnOff)?.operations!!
// for (operation in operations) {
// DropdownMenuItem(
// text = { Text(operation.toString()) },
// onClick = {
// scope.launch {
// starterOperation.value = operation
// }
// expandedOperationSelection = false
// }
// )
// }
}
Quita la marca de comentario del paso 4.1.4 en el archivo StarterView.kt
para renderizar todos los valores del rasgo seleccionado y, luego, implementa la devolución de llamada de clic en un DropdownMenu
:
when (starterTrait.value) {
OnOff -> {
...
DropdownMenu(expanded = expandedBooleanSelection, onDismissRequest = { expandedBooleanSelection = false }) {
// TODO: 4.1.4 - Starter device trait values selection dropdown
// for (value in StarterViewModel.valuesOnOff.keys) {
// DropdownMenuItem(
// text = { Text(value.toString()) },
// onClick = {
// scope.launch {
// starterValueOnOff.value = StarterViewModel.valuesOnOff.get(value)
// }
// expandedBooleanSelection = false
// }
// )
// }
}
...
}
LevelControl -> {
...
}
}
Quita la marca de comentario del paso 4.1.5 en el archivo StarterView.kt
para almacenar todas las variables de inicio ViewModel
en el inicio ViewModel
de la automatización de borrador (draftVM.starterVMs
).
val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save starter button:
Button(
enabled = isOptionsSelected && isValueProvided,
onClick = {
scope.launch {
// TODO: 4.1.5 - store all starter ViewModel variables into draft ViewModel
// starterVM.deviceVM.emit(starterDeviceVM.value)
// starterVM.trait.emit(starterTrait.value)
// starterVM.operation.emit(starterOperation.value)
// starterVM.valueOnOff.emit(starterValueOnOff.value!!)
// starterVM.valueLevel.emit(starterValueLevel.value!!)
// starterVM.valueBooleanState.emit(starterValueBooleanState.value!!)
// starterVM.valueOccupancy.emit(starterValueOccupancy.value!!)
// starterVM.valueThermostat.emit(starterValueThermostat.value!!)
//
// draftVM.starterVMs.value.add(starterVM)
// draftVM.selectedStarterVM.emit(null)
}
})
{ Text(stringResource(R.string.starter_button_create)) }
Si ejecutas la app y seleccionas una nueva automatización y un nuevo activador, debería mostrarse una vista como la siguiente:
La app de muestra solo admite iniciadores basados en características del dispositivo.
Configura acciones
La acción de automatización refleja el propósito central de una automatización, cómo afecta un cambio en el mundo físico. En la app de ejemplo, capturamos las acciones de automatización con la clase ActionViewModel
y mostramos la vista del editor con la clase ActionView
.
La app de ejemplo usa las siguientes entidades de las APIs de Home para definir los nodos de acción de automatización:
- Dispositivo
- Rasgo
- Comando
- Valor (opcional)
Cada acción de comando del dispositivo usa un comando, pero algunos también requerirán un valor de parámetro asociado, como MoveToLevel()
y un porcentaje objetivo.
El dispositivo y el rasgo se pueden seleccionar de los objetos que devuelve la API de Devices.
La app define una lista predefinida de comandos:
// List of operations available when creating automation starters:
enum class Action {
ON,
OFF,
MOVE_TO_LEVEL,
MODE_HEAT,
MODE_COOL,
MODE_OFF,
}
La app realiza un seguimiento de las operaciones admitidas para cada rasgo admitido:
// List of operations available when comparing booleans:
object OnOffActions : Actions(listOf(
Action.ON,
Action.OFF,
))
// List of operations available when comparing booleans:
object LevelActions : Actions(listOf(
Action.MOVE_TO_LEVEL
))
// List of operations available when comparing booleans:
object ThermostatActions : Actions(listOf(
Action.MODE_HEAT,
Action.MODE_COOL,
Action.MODE_OFF,
))
// Map traits and the comparison operations they support:
val actionActions: Map<TraitFactory<out Trait>, Actions> = mapOf(
OnOff to OnOffActions,
LevelControl to LevelActions,
// BooleanState - No Actions
// OccupancySensing - No Actions
Thermostat to ThermostatActions,
)
Para los comandos que toman uno o más parámetros, también hay una variable:
val valueLevel: MutableStateFlow<UByte?>
La API muestra un conjunto de elementos de vista que los usuarios pueden usar para seleccionar los campos requeridos.
Quita la marca de comentario del paso 4.2.1 en el archivo ActionView.kt
para renderizar todos los dispositivos de acción y, luego, implementa la devolución de llamada de clic en un DropdownMenu
para establecer actionDeviceVM
.
val deviceVMs = structureVM.deviceVMs.collectAsState().value
...
DropdownMenu(expanded = expandedDeviceSelection, onDismissRequest = { expandedDeviceSelection = false }) {
// TODO: 4.2.1 - Action device selection dropdown
// for (deviceVM in deviceVMs) {
// DropdownMenuItem(
// text = { Text(deviceVM.name) },
// onClick = {
// scope.launch {
// actionDeviceVM.value = deviceVM
// actionTrait.value = null
// actionAction.value = null
// }
// expandedDeviceSelection = false
// }
// )
// }
}
Quita la marca de comentario del paso 4.2.2 en el archivo ActionView.kt
para renderizar todos los rasgos de actionDeviceVM
y, luego, implementa la devolución de llamada de clic en un DropdownMenu
para establecer el actionTrait
, que representa el rasgo al que pertenece el comando.
val actionDeviceVM: MutableState<DeviceViewModel?> = remember {
mutableStateOf(actionVM.deviceVM.value) }
...
DropdownMenu(expanded = expandedTraitSelection, onDismissRequest = { expandedTraitSelection = false }) {
// TODO: 4.2.2 - Action device traits selection dropdown
// val deviceTraits: List<Trait> = actionDeviceVM.value?.traits?.collectAsState()?.value!!
// for (trait in deviceTraits) {
// DropdownMenuItem(
// text = { Text(trait.factory.toString()) },
// onClick = {
// scope.launch {
// actionTrait.value = trait
// actionAction.value = null
// }
// expandedTraitSelection = false
// }
// )
// }
}
Quita la marca de comentario del paso 4.2.3 en el archivo ActionView.kt
para renderizar todas las acciones disponibles de actionTrait
y, luego, implementa la devolución de llamada de clic en un DropdownMenu
para establecer el actionAction
, que representa la acción de automatización seleccionada.
DropdownMenu(expanded = expandedActionSelection, onDismissRequest = { expandedActionSelection = false }) {
// ...
if (!ActionViewModel.actionActions.containsKey(actionTrait.value?.factory))
return@DropdownMenu
// TODO: 4.2.3 - Action device trait actions (commands) selection dropdown
// val actions: List<ActionViewModel.Action> = ActionViewModel.actionActions.get(actionTrait.value?.factory)?.actions!!
// for (action in actions) {
// DropdownMenuItem(
// text = { Text(action.toString()) },
// onClick = {
// scope.launch {
// actionAction.value = action
// }
// expandedActionSelection = false
// }
// )
// }
}
Quita el comentario del paso 4.2.4 en el archivo ActionView.kt
para renderizar los valores disponibles de la acción del rasgo (comando) y almacenar el valor en actionValueLevel
en la devolución de llamada del cambio de valor:
when (actionTrait.value?.factory) {
LevelControl -> {
// TODO: 4.2.4 - Action device trait action(command) values selection widget
// Column (Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth()) {
// Text(stringResource(R.string.action_title_value), fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
// }
//
// Box (Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
// LevelSlider(value = actionValueLevel.value?.toFloat()!!, low = 0f, high = 254f, steps = 0,
// modifier = Modifier.padding(top = 16.dp),
// onValueChange = { value : Float -> actionValueLevel.value = value.toUInt().toUByte() }
// isEnabled = true
// )
// }
...
}
Quita la marca de comentario del paso 4.2.5 en el archivo ActionView.kt
para almacenar todas las variables de ViewModel
de la acción en el ViewModel
de la acción del borrador de automatización (draftVM.actionVMs
):
val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
// Save action button:
Button(
enabled = isOptionsSelected,
onClick = {
scope.launch {
// TODO: 4.2.5 - store all action ViewModel variables into draft ViewModel
// actionVM.deviceVM.emit(actionDeviceVM.value)
// actionVM.trait.emit(actionTrait.value)
// actionVM.action.emit(actionAction.value)
// actionVM.valueLevel.emit(actionValueLevel.value)
//
// draftVM.actionVMs.value.add(actionVM)
// draftVM.selectedActionVM.emit(null)
}
})
{ Text(stringResource(R.string.action_button_create)) }
Si ejecutas la app y seleccionas una nueva automatización y acción, deberías ver una vista como la siguiente:
En la app de ejemplo, solo admitimos acciones basadas en características del dispositivo.
Renderiza una automatización de borrador
Cuando se completa el DraftViewModel
, HomeAppView.kt
puede renderizarlo:
fun HomeAppView (homeAppVM: HomeAppViewModel) {
...
// If a draft automation is selected, show the draft editor:
if (selectedDraftVM != null) {
DraftView(homeAppVM)
}
...
}
En DraftView.kt
:
fun DraftView (homeAppVM: HomeAppViewModel) {
val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
...
// Draft Starters:
DraftStarterList(draftVM)
// Draft Actions:
DraftActionList(draftVM)
}
Crea una automatización
Ahora que aprendiste a crear activadores y acciones, puedes crear un borrador de automatización y enviarlo a la API de Automation. La API tiene una función createAutomation()
que toma un borrador de automatización como argumento y devuelve una nueva instancia de automatización.
La preparación del borrador de automatización se lleva a cabo en la clase DraftViewModel
de la app de ejemplo. Consulta la función getDraftAutomation()
para obtener más información sobre cómo estructuramos el borrador de automatización con las variables de inicio y acción de la sección anterior.
Quita la marca de comentario del paso 4.4.1 en el archivo DraftViewModel.kt
para crear las expresiones "select" necesarias para crear el gráfico de automatización cuando el rasgo inicial es OnOff
:
val starterVMs: List<StarterViewModel> = starterVMs.value
val actionVMs: List<ActionViewModel> = actionVMs.value
...
fun getDraftAutomation() : DraftAutomation {
...
val starterVMs: List<StarterViewModel> = starterVMs.value
...
return automation {
this.name = name
this.description = description
this.isActive = true
// The sequential block wrapping all nodes:
sequential {
// The select block wrapping all starters:
select {
// Iterate through the selected starters:
for (starterVM in starterVMs) {
// The sequential block for each starter (should wrap the Starter Expression!)
sequential {
...
val starterTrait: TraitFactory<out Trait> = starterVM.trait.value!!
...
when (starterTrait) {
OnOff -> {
// TODO: 4.4.1 - Set starter expressions according to trait type
// val onOffValue: Boolean = starterVM.valueOnOff.value
// val onOffExpression: TypedExpression<out OnOff> =
// starterExpression as TypedExpression<out OnOff>
// when (starterOperation) {
// StarterViewModel.Operation.EQUALS ->
// condition { expression = onOffExpression.onOff equals onOffValue }
// StarterViewModel.Operation.NOT_EQUALS ->
// condition { expression = onOffExpression.onOff notEquals onOffValue }
// else -> { MainActivity.showError(this, "Unexpected operation for OnOf
// }
}
LevelControl -> {
...
// Function to allow manual execution of the automation:
manualStarter()
...
}
Quita la marca de comentario del paso 4.4.2 en el archivo DraftViewModel.kt
para crear las expresiones paralelas necesarias para crear el grafo de automatización cuando el rasgo de acción seleccionado es LevelControl
y la acción seleccionada es MOVE_TO_LEVEL
:
val starterVMs: List<StarterViewModel> = starterVMs.value
val actionVMs: List<ActionViewModel> = actionVMs.value
...
fun getDraftAutomation() : DraftAutomation {
...
return automation {
this.name = name
this.description = description
this.isActive = true
// The sequential block wrapping all nodes:
sequential {
...
// Parallel block wrapping all actions:
parallel {
// Iterate through the selected actions:
for (actionVM in actionVMs) {
val actionDeviceVM: DeviceViewModel = actionVM.deviceVM.value!!
// Action Expression that the DSL will check for:
action(actionDeviceVM.device, actionDeviceVM.type.value.factory) {
val actionCommand: Command = when (actionVM.action.value) {
ActionViewModel.Action.ON -> { OnOff.on() }
ActionViewModel.Action.OFF -> { OnOff.off() }
// TODO: 4.4.2 - Set starter expressions according to trait type
// ActionViewModel.Action.MOVE_TO_LEVEL -> {
// LevelControl.moveToLevelWithOnOff(
// actionVM.valueLevel.value!!,
// 0u,
// LevelControlTrait.OptionsBitmap(),
// LevelControlTrait.OptionsBitmap()
// )
// }
ActionViewModel.Action.MODE_HEAT -> { SimplifiedThermostat
.setSystemMode(SimplifiedThermostatTrait.SystemModeEnum.Heat) }
...
}
El último paso para completar una automatización es implementar la función getDraftAutomation
para crear un AutomationDraft.
.
Quita la marca de comentario del paso 4.4.3 en el archivo HomeAppViewModel.kt
para crear la automatización llamando a las APIs de Home y controlando las excepciones:
fun createAutomation(isPending: MutableState<Boolean>) {
viewModelScope.launch {
val structure : Structure = selectedStructureVM.value?.structure!!
val draft : DraftAutomation = selectedDraftVM.value?.getDraftAutomation()!!
isPending.value = true
// TODO: 4.4.3 - Call the Home API to create automation and handle exceptions
// // Call Automation API to create an automation from a draft:
// try {
// structure.createAutomation(draft)
// }
// catch (e: Exception) {
// MainActivity.showError(this, e.toString())
// isPending.value = false
// return@launch
// }
// Scrap the draft and automation candidates used in the process:
selectedCandidateVMs.emit(null)
selectedDraftVM.emit(null)
isPending.value = false
}
}
Ahora ejecuta la app y observa los cambios en tu dispositivo.
Una vez que selecciones un activador y una acción, podrás crear la automatización:
Asegúrate de asignarle a tu automatización un nombre único y, luego, presiona el botón Crear automatización, que debería llamar a las APIs y llevarte de vuelta a la vista de lista de automatizaciones con tu automatización:
Presiona la automatización que acabas de crear y observa cómo la devuelven las APIs.
Ten en cuenta que la API devuelve un valor que indica si una automatización es válida y está activa en este momento. Es posible crear automatizaciones que no pasen la validación cuando se analizan en el servidor. Si un análisis de automatización no pasa la validación, isValid
se establece en false
, lo que indica que la automatización no es válida y está inactiva. Si tu automatización no es válida, consulta el campo automation.validationIssues
para obtener más detalles.
Asegúrate de que tu automatización esté configurada como válida y activa, y, luego, puedes probarla.
Prueba tu automatización
Las automatizaciones se pueden ejecutar de dos maneras:
- Con un evento de inicio Si las condiciones coinciden, se activa la acción que estableciste en la automatización.
- Con una llamada a la API de ejecución manual
Si una automatización de borrador tiene un manualStarter()
definido en el bloque DSL de borrador de automatización, el motor de automatización admitirá la ejecución manual para esa automatización. Esto ya está presente en los ejemplos de código de la app de ejemplo.
Como aún estás en la pantalla de vista de automatización en tu dispositivo móvil, presiona el botón Ejecutar manualmente. Esto debería llamar a automation.execute()
, que ejecuta el comando de acción en el dispositivo que seleccionaste cuando configuraste la automatización.
Una vez que valides el comando de acción a través de la ejecución manual con la API, es hora de ver si también se ejecuta con el iniciador que definiste.
Ve a la pestaña Dispositivos, selecciona el dispositivo de acción y el rasgo, y configúralo con un valor diferente (por ejemplo, configura el LevelControl
(brillo) de light2
en un 50%, como se ilustra en la siguiente captura de pantalla:
Ahora intentaremos activar la automatización con el dispositivo de inicio. Elige el dispositivo activador que seleccionaste cuando creaste la automatización. Activa el rasgo que elegiste (por ejemplo, establece el OnOff
de starter outlet1
en On
):
Verás que esto también ejecuta la automatización y establece el rasgo LevelControl
del dispositivo de acción light2
en el valor original, 100%:
¡Felicitaciones! Usaste correctamente las APIs de Home para crear automatizaciones.
Para obtener más información sobre la API de Automation, consulta API de Automation de Android.
5. Descubre las capacidades
Las APIs de Home incluyen una API dedicada llamada Discovery API, que los desarrolladores pueden usar para consultar qué rasgos compatibles con la automatización se admiten en un dispositivo determinado. La app de ejemplo proporciona un ejemplo en el que puedes usar esta API para descubrir qué comandos están disponibles.
Descubre comandos
En esta sección, analizamos cómo descubrir los CommandCandidates
compatibles y cómo crear una automatización basada en los nodos candidatos descubiertos.
En la app de ejemplo, llamamos a device.candidates()
para obtener una lista de candidatos, que puede incluir instancias de CommandCandidate
, EventCandidate
o TraitAttributesCandidate
.
Ve al archivo HomeAppViewModel.kt
y quita el comentario del paso 5.1.1 para recuperar la lista de candidatos y filtrar con el tipo Candidate
:
fun showCandidates() {
...
// TODO: 5.1.1 - Retrieve automation candidates, filtering to include CommandCandidate types only
// // Retrieve a set of initial automation candidates from the device:
// val candidates: Set<NodeCandidate> = deviceVM.device.candidates().first()
//
// for (candidate in candidates) {
// // Check whether the candidate trait is supported:
// if(candidate.trait !in HomeApp.supportedTraits)
// continue
// // Check whether the candidate type is supported:
// when (candidate) {
// // Command candidate type:
// is CommandCandidate -> {
// // Check whether the command candidate has a supported command:
// if (candidate.commandDescriptor !in ActionViewModel.commandMap)
// continue
// }
// // Other candidate types are currently unsupported:
// else -> { continue }
// }
//
// candidateVMList.add(CandidateViewModel(candidate, deviceVM))
// }
...
// Store the ViewModels:
selectedCandidateVMs.emit(candidateVMList)
}
Observa cómo se filtra el CommandCandidate.
Los candidatos que devuelve la API pertenecen a diferentes tipos. La app de ejemplo admite CommandCandidate
. Quita la marca de comentario del paso 5.1.2 en el commandMap
definido en ActionViewModel.kt
para establecer estos rasgos admitidos:
// Map of supported commands from Discovery API:
val commandMap: Map<CommandDescriptor, Action> = mapOf(
// TODO: 5.1.2 - Set current supported commands
// OnOffTrait.OnCommand to Action.ON,
// OnOffTrait.OffCommand to Action.OFF,
// LevelControlTrait.MoveToLevelWithOnOffCommand to Action.MOVE_TO_LEVEL
)
Ahora que podemos llamar a la API de Discovery y filtrar los resultados que admitimos en la app de ejemplo, analizaremos cómo podemos integrar esto en nuestro editor.
Para obtener más información sobre la API de Discovery, consulta Aprovecha la detección de dispositivos en Android.
Integra el editor
La forma más común de usar las acciones descubiertas es presentarlas a un usuario final para que las seleccione. Justo antes de que el usuario seleccione los campos de automatización del borrador, podemos mostrarle la lista de acciones descubiertas y, según el valor que seleccione, podemos completar previamente el nodo de acción en el borrador de automatización.
El archivo CandidatesView.kt
contiene la clase de vista que muestra los candidatos descubiertos. Quita la marca de comentario del paso 5.2.1 para habilitar la función .clickable{}
de CandidateListItem
, que establece homeAppVM.selectedDraftVM
como candidateVM
:
fun CandidateListItem (candidateVM: CandidateViewModel, homeAppVM: HomeAppViewModel) {
val scope: CoroutineScope = rememberCoroutineScope()
Box (Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) {
Column (Modifier.fillMaxWidth().clickable {
// TODO: 5.2.1 - Set the selectedDraftVM to the selected candidate
// scope.launch { homeAppVM.selectedDraftVM.emit(DraftViewModel(candidateVM)) }
}) {
...
}
}
}
De manera similar al paso 4.3 en HomeAppView.kt
, cuando se establece el selectedDraftVM
, se renderiza el DraftView(...) in
DraftView.kt:
fun HomeAppView (homeAppVM: HomeAppViewModel) {
...
val selectedDraftVM: DraftViewModel? by homeAppVM.selectedDraftVM.collectAsState()
...
// If a draft automation is selected, show the draft editor:
if (selectedDraftVM != null) {
DraftView(homeAppVM)
}
...
}
Vuelve a intentarlo. Para ello, presiona light2 - MOVE_TO_LEVEL, que se muestra en la sección anterior y te solicita que crees una nueva automatización basada en el comando del candidato:
Ahora que ya conoces la creación de automatizaciones en la app de ejemplo, puedes integrar automatizaciones en tus apps.
6. Ejemplos de automatización avanzada
Antes de terminar, analizaremos algunos ejemplos adicionales del DSL de automatización. Estos ejemplos ilustran algunas de las capacidades avanzadas que puedes lograr con las APIs.
La hora del día como activador
Además de los rasgos del dispositivo, las APIs de Google Home ofrecen rasgos basados en la estructura, como Time
. Puedes crear una automatización que tenga un activador basado en el tiempo, como el siguiente:
automation {
name = "AutomationName"
description = "An example automation description."
isActive = true
description = "Do ... actions when time is up."
sequential {
// starter
val starter = starter<_>(structure, Time.ScheduledTimeEvent) {
parameter(
Time.ScheduledTimeEvent.clockTime(
LocalTime.of(hour, min, sec, 0)
)
)
}
// action
...
}
}
Transmisión de Asistente como acción
El rasgo AssistantBroadcast
está disponible como rasgo a nivel del dispositivo en un SpeakerDevice
(si la bocina lo admite) o como rasgo a nivel de la estructura (porque las bocinas de Google y los dispositivos móviles Android pueden reproducir transmisiones del Asistente). Por ejemplo:
automation {
name = "AutomationName"
description = "An example automation description."
isActive = true
description = "Broadcast in Speaker when ..."
sequential {
// starter
...
// action
action(structure) {
command(
AssistantBroadcast.broadcast("Time is up!!")
)
}
}
}
Pueden usar DelayFor
y suppressFor
La API de Automation también proporciona operadores avanzados, como delayFor, que se usa para retrasar comandos, y suppressFor, que puede impedir que una automatización se active por los mismos eventos dentro de un período determinado. Estos son algunos ejemplos del uso de estos operadores:
sequential {
val starterNode = starter<_>(device, OccupancySensorDevice, MotionDetection)
// only proceed if there is currently motion taking place
condition { starterNode.motionDetectionEventInProgress equals true }
// ignore the starter for one minute after it was last triggered
suppressFor(Duration.ofMinutes(1))
// make announcements three seconds apart
action(device, SpeakerDevice) {
command(AssistantBroadcast.broadcast("Intruder detected!"))
}
delayFor(Duration.ofSeconds(3))
action(device, SpeakerDevice) {
command(AssistantBroadcast.broadcast("Intruder detected!"))
}
...
}
Usa AreaPresenceState
en un activador
AreaPresenceState
es un rasgo a nivel de la estructura que detecta si hay alguien en casa.
Por ejemplo, el siguiente ejemplo muestra cómo trabar las puertas automáticamente cuando alguien está en casa después de las 10 p.m.:
automation {
name = "Lock the doors when someone is home after 10pm"
description = "1 starter, 2 actions"
sequential {
val unused =
starter(structure, event = Time.ScheduledTimeEvent) {
parameter(Time.ScheduledTimeEvent.clockTime(LocalTime.of(22, 0, 0, 0)))
}
val stateReaderNode = stateReader<_>(structure, AreaPresenceState)
condition {
expression =
stateReaderNode.presenceState equals
AreaPresenceStateTrait.PresenceState.PresenceStateOccupied
}
action(structure) { command(AssistantBroadcast.broadcast("Locks are being applied")) }
for (lockDevice in lockDevices) {
action(lockDevice, DoorLockDevice) {
command(Command(DoorLock, DoorLockTrait.LockDoorCommand.requestId.toString(), mapOf()))
}
}
}
Ahora que conoces estas capacidades avanzadas de automatización, puedes crear apps increíbles.
7. ¡Felicitaciones!
¡Felicitaciones! Completaste correctamente la segunda parte del desarrollo de una app para Android con las APIs de Google Home. A lo largo de este codelab, exploraste las APIs de Automation y Discovery.
Esperamos que disfrutes crear apps que controlen dispositivos de forma creativa dentro del ecosistema de Google Home y crear situaciones de automatización emocionantes con las APIs de Home.
Próximos pasos
- Lee Solución de problemas para obtener información sobre cómo depurar apps y solucionar problemas relacionados con las APIs de Home de manera eficaz.
- Puedes comunicarte con nosotros para enviarnos recomendaciones o informar problemas a través del seguimiento de problemas, tema de asistencia para la casa inteligente.