1. Avant de commencer
Il s'agit du deuxième atelier de programmation de la série sur la création d'une application Android à l'aide des API Google Home. Dans cet atelier de programmation, nous vous expliquons comment créer des automatisations pour la maison connectée et vous donnons quelques conseils sur les bonnes pratiques à suivre pour utiliser les API. Si vous n'avez pas encore terminé le premier atelier de programmation, Créer une application mobile à l'aide des API Home sur Android, nous vous recommandons de le faire avant de commencer celui-ci.
Les API Google Home fournissent un ensemble de bibliothèques permettant aux développeurs Android de contrôler les appareils connectés dans l'écosystème Google Home. Grâce à ces nouvelles API, les développeurs pourront définir des automatisations pour une maison connectée qui peuvent contrôler les fonctionnalités des appareils en fonction de conditions prédéfinies. Google fournit également une API Discovery qui vous permet d'interroger les appareils pour connaître les attributs et les commandes qu'ils acceptent.
Prérequis
- Vous avez suivi l'atelier de programmation Créer une application mobile à l'aide des API Home sur Android.
- Connaissances de l'écosystème Google Home (cloud-to-cloud et Matter)
- Une station de travail sur laquelle Android Studio (2024.3.1 Ladybug ou version ultérieure) est installé.
- Un téléphone Android conforme aux exigences des API Home (voir la section Conditions préalables), avec les services Google Play et l'application Google Home installés.
- Un Google Home Hub compatible avec les API Google Home.
- Facultatif : un appareil connecté compatible avec les API Google Home.
Points abordés
- Créer des automatisations pour les appareils connectés à l'aide des API Home
- Utiliser les API Discovery pour explorer les fonctionnalités de l'appareil compatibles
- Comment suivre les bonnes pratiques lorsque vous créez vos applications avec les API Home
2. Configurer le projet
Le schéma suivant illustre l'architecture d'une application utilisant les API Home:
- Code de l'application:code principal sur lequel les développeurs travaillent pour créer l'interface utilisateur de l'application et la logique d'interaction avec le SDK des API Home.
- SDK Home APIs:le SDK Home APIs fourni par Google fonctionne avec le service Home APIs de GMSCore pour contrôler les appareils de maison connectée. Les développeurs créent des applications compatibles avec les API Home en les regroupant avec le SDK des API Home.
- GMSCore sur Android:GMSCore, également appelé services Google Play, est une plate-forme Google qui fournit des services système essentiels, ce qui permet de bénéficier de fonctionnalités clés sur tous les appareils Android certifiés. Le module d'accueil des services Google Play contient les services qui interagissent avec les API Home.
Dans cet atelier de programmation, nous allons nous appuyer sur ce que nous avons vu dans Créer une application mobile à l'aide des API Home sur Android.
Assurez-vous que vous disposez d'une structure avec au moins deux appareils compatibles configurés et opérationnels dans le compte. Comme nous allons configurer des automatisations dans cet atelier de programmation (un changement de l'état d'un appareil déclenche une action sur un autre), vous aurez besoin de deux appareils pour voir les résultats.
Obtenir l'application exemple
Le code source de l'application exemple est disponible sur GitHub dans le dépôt google-home/google-home-api-sample-app-android.
Cet atelier de programmation utilise les exemples de la branche codelab-branch-2
de l'application exemple.
Accédez à l'emplacement où vous souhaitez enregistrer le projet, puis clonez la branche codelab-branch-2
:
$ git clone -b codelab-branch-2 https://github.com/google-home/google-home-api-sample-app-android.git
Notez qu'il s'agit d'une branche différente de celle utilisée dans Créer une application mobile à l'aide des API Home sur Android. Cette branche du codebase s'appuie sur le point où s'était arrêté le premier atelier de programmation. Cette fois, les exemples vous expliquent comment créer des automatisations. Si vous avez terminé l'atelier de programmation précédent et que vous avez réussi à faire fonctionner toutes les fonctionnalités, vous pouvez choisir d'utiliser le même projet Android Studio pour cet atelier plutôt que codelab-branch-2
.
Une fois le code source compilé et prêt à s'exécuter sur votre appareil mobile, passez à la section suivante.
3. En savoir plus sur les automatisations
Les automatisations sont un ensemble d'énoncés "si ceci, alors cela" qui peuvent contrôler automatiquement l'état des appareils en fonction de facteurs sélectionnés. Les développeurs peuvent utiliser les automatisations pour créer des fonctionnalités interactives avancées dans leurs API.
Les automatisations sont constituées de trois types de composants appelés nodes: déclencheurs, actions et conditions. Ces nœuds fonctionnent ensemble pour automatiser les comportements à l'aide d'appareils connectés. En règle générale, elles sont évaluées dans l'ordre suivant:
- Starter : définit les conditions initiales qui activent l'automatisation, comme une modification de la valeur d'un trait. Une automatisation doit comporter un Starter.
- Condition : toute contrainte supplémentaire à évaluer après le déclenchement d'une automatisation. L'expression d'une condition doit renvoyer la valeur "true" pour que les actions d'une automatisation s'exécutent.
- Action : commandes ou mises à jour d'état exécutées lorsque toutes les conditions sont remplies.
Par exemple, vous pouvez créer une automatisation qui atténue les lumières d'une pièce lorsqu'un interrupteur est activé, tandis que la télévision de cette pièce est allumée. Dans cet exemple :
- Starter : le bouton de la pièce est activé.
- Condition : l'état OnOff du téléviseur est évalué comme étant "On".
- Action : les lumières de la pièce où se trouve le contacteur sont atténuées.
Ces nœuds sont évalués par le moteur d'automatisation de manière séquentielle ou parallèle.
Un flux séquentiel contient des nœuds qui s'exécutent dans l'ordre séquentiel. Il s'agit généralement d'un déclencheur, d'une condition et d'une action.
Un flux parallèle peut comporter plusieurs nœuds d'action exécutés simultanément, par exemple pour allumer plusieurs lumières en même temps. Les nœuds qui suivent un flux parallèle ne s'exécutent pas tant que toutes les branches du flux parallèle ne sont pas terminées.
Le schéma d'automatisation contient d'autres types de nœuds. Pour en savoir plus, consultez la section Nœuds du guide du développeur des API Home. De plus, les développeurs peuvent combiner différents types de nœuds pour créer des automatisations complexes, par exemple:
Les développeurs fournissent ces nœuds au moteur d'automatisation à l'aide d'un langage spécifique au domaine (DSL) créé spécifiquement pour les automatisations Google Home.
Découvrir le DSL d'automatisation
Un langage spécifique à un domaine (DSL, domain-specific language) est un langage utilisé pour capturer le comportement du système dans le code. Le compilateur génère des classes de données sérialisées au format JSON de tampon de protocole et utilisées pour effectuer des appels aux services d'automatisation de Google.
Le DSL recherche le schéma suivant:
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()) }
}
}
L'automatisation de l'exemple précédent synchronise deux ampoules. Lorsque l'état OnOff
de device1
passe à On
(onOffTrait.onOff equals true
), l'état OnOff
de device2
passe à On
(command(OnOff.on()
).
Lorsque vous travaillez avec des automatisations, sachez qu'il existe des limites de ressources.
Les automatisations sont un outil très utile pour créer des fonctionnalités automatisées dans une maison connectée. Dans le cas d'utilisation le plus simple, vous pouvez coder explicitement une automatisation pour qu'elle utilise des appareils et des caractéristiques spécifiques. Toutefois, un cas d'utilisation plus pratique est celui où l'application permet à l'utilisateur de configurer les appareils, les commandes et les paramètres d'une automatisation. La section suivante explique comment créer un éditeur d'automatisation qui permet à l'utilisateur de faire exactement cela.
4. Créer un éditeur d'automatisations
Dans l'application exemple, nous allons créer un éditeur d'automatisation permettant aux utilisateurs de sélectionner les appareils, les fonctionnalités (actions) qu'ils souhaitent utiliser et la manière dont les automatisations sont déclenchées à l'aide de déclencheurs.
Configurer des déclencheurs
Le déclencheur d'automatisation est le point d'entrée de l'automatisation. Un déclencheur déclenche une automatisation lorsqu'un événement donné se produit. Dans l'application exemple, nous capturons les déclencheurs d'automatisation à l'aide de la classe StarterViewModel
, qui se trouve dans le fichier source StarterViewModel.kt
, et affichons la vue de l'éditeur à l'aide de StarterView
(StarterView.kt
).
Un nœud de démarrage nécessite les éléments suivants:
- Appareil
- Trait
- Opération
- Valeur
L'appareil et la caractéristique peuvent être sélectionnés parmi les objets renvoyés par l'API Devices. Les commandes et les paramètres de chaque appareil compatible sont plus complexes et doivent être gérés séparément.
L'application définit une liste d'opérations prédéfinies:
// 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
}
Pour chaque trait compatible, il suit les opérations compatibles:
// 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 même, l'application exemple suit les valeurs pouvant être attribuées aux traits:
enum class OnOffValue {
On,
Off,
}
enum class ThermostatValue {
Heat,
Cool,
Off,
}
Il suit également un mappage entre les valeurs définies par l'application et celles définies par les API:
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,
)
L'application affiche ensuite un ensemble d'éléments de vue que les utilisateurs peuvent utiliser pour sélectionner les champs obligatoires.
Décommentez l'étape 4.1.1 du fichier StarterView.kt
pour afficher tous les appareils de démarrage et implémenter le rappel de clic dans 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
// }
// )
// }
}
Décomentez l'étape 4.1.2 dans le fichier StarterView.kt
pour afficher tous les traits de l'appareil de démarrage et implémenter le rappel de clic dans 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
// }
// )
}
}
Décommentez l'étape 4.1.3 dans le fichier StarterView.kt
pour afficher toutes les opérations du trait sélectionné et implémenter le rappel de clic dans 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
// }
// )
// }
}
Décommentez l'étape 4.1.4 du fichier StarterView.kt
pour afficher toutes les valeurs du trait sélectionné et implémentez le rappel de clic dans 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 -> {
...
}
}
Décommentez l'étape 4.1.5 dans le fichier StarterView.kt
pour stocker toutes les variables ViewModel
du déclencheur dans le déclencheur ViewModel
(draftVM.starterVMs
) de l'automatisation d'ébauche.
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)) }
Exécutez l'application et sélectionnez une nouvelle automatisation et un nouveau déclencheur. Une vue semblable à celle-ci devrait s'afficher:
L'application exemple n'est compatible qu'avec les déclencheurs basés sur les caractéristiques de l'appareil.
Configurer des actions
L'action d'automatisation reflète l'objectif principal d'une automatisation, c'est-à-dire comment elle provoque un changement dans le monde physique. Dans l'application exemple, nous capturons les actions d'automatisation à l'aide de la classe ActionViewModel
et affichons la vue de l'éditeur à l'aide de la classe ActionView
.
L'application exemple utilise les entités des API Home suivantes pour définir les nœuds d'action d'automatisation:
- Appareil
- Trait
- Commande
- Valeur (facultatif)
Chaque action de commande d'appareil utilise une commande, mais certaines nécessitent également une valeur de paramètre associée, comme MoveToLevel()
et un pourcentage cible.
L'appareil et la caractéristique peuvent être sélectionnés parmi les objets renvoyés par l'API Devices.
L'application définit une liste de commandes prédéfinies:
// List of operations available when creating automation starters:
enum class Action {
ON,
OFF,
MOVE_TO_LEVEL,
MODE_HEAT,
MODE_COOL,
MODE_OFF,
}
L'application suit les opérations compatibles pour chaque caractéristique compatible:
// 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,
)
Pour les commandes qui acceptent un ou plusieurs paramètres, il existe également une variable:
val valueLevel: MutableStateFlow<UByte?>
L'API affiche un ensemble d'éléments de vue que les utilisateurs peuvent utiliser pour sélectionner les champs obligatoires.
Décomentez l'étape 4.2.1 dans le fichier ActionView.kt
pour afficher tous les appareils d'action et implémentez le rappel de clic dans un DropdownMenu
pour définir 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
// }
// )
// }
}
Décommentez l'étape 4.2.2 dans le fichier ActionView.kt
pour afficher tous les traits de actionDeviceVM
et implémentez le rappel de clic dans un DropdownMenu
pour définir le actionTrait
, qui représente le trait auquel la commande appartient.
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
// }
// )
// }
}
Décommentez l'étape 4.2.3 dans le fichier ActionView.kt
pour afficher toutes les actions disponibles de actionTrait
et implémentez le rappel de clic dans un DropdownMenu
pour définir le actionAction
, qui représente l'action d'automatisation sélectionnée.
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
// }
// )
// }
}
Annulez la mise en commentaire de l'étape 4.2.4 dans le fichier ActionView.kt
pour afficher les valeurs disponibles de l'action de trait (commande) et stocker la valeur dans actionValueLevel
dans le rappel de modification de valeur:
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
// )
// }
...
}
Décommentez l'étape 4.2.5 dans le fichier ActionView.kt
pour stocker toutes les variables de l'action ViewModel
dans l'action ViewModel
(draftVM.actionVMs
) de l'automatisation de l'aperçu :
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 vous exécutez l'application et sélectionnez une nouvelle automatisation et une nouvelle action, une vue semblable à celle-ci devrait s'afficher:
Dans l'application exemple, nous n'acceptons que les actions basées sur les caractéristiques de l'appareil.
Afficher une automatisation en brouillon
Une fois l'DraftViewModel
terminée, elle peut être affichée par HomeAppView.kt
:
fun HomeAppView (homeAppVM: HomeAppViewModel) {
...
// If a draft automation is selected, show the draft editor:
if (selectedDraftVM != null) {
DraftView(homeAppVM)
}
...
}
Dans DraftView.kt
:
fun DraftView (homeAppVM: HomeAppViewModel) {
val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
...
// Draft Starters:
DraftStarterList(draftVM)
// Draft Actions:
DraftActionList(draftVM)
}
Créer une automatisation
Maintenant que vous savez créer des déclencheurs et des actions, vous êtes prêt à créer un brouillon d'automatisation et à l'envoyer à l'API Automation. L'API dispose d'une fonction createAutomation()
qui accepte un brouillon d'automatisation comme argument et renvoie une nouvelle instance d'automatisation.
La préparation de l'ébauche d'automatisation a lieu dans la classe DraftViewModel
de l'application exemple. Consultez la fonction getDraftAutomation()
pour en savoir plus sur la façon dont nous structurons l'ébauche d'automatisation à l'aide des variables de déclencheur et d'action de la section précédente.
Décomentez l'étape 4.4.1 du fichier DraftViewModel.kt
pour créer les expressions "select" requises pour créer le graphique d'automatisation lorsque le trait de déclencheur est 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()
...
}
Décommentez l'étape 4.4.2 dans le fichier DraftViewModel.kt
pour créer les expressions parallèles requises pour créer le graphique d'automatisation lorsque le trait d'action sélectionné est LevelControl
et que l'action sélectionnée est 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) }
...
}
La dernière étape de l'automatisation consiste à implémenter la fonction getDraftAutomation
pour créer un AutomationDraft.
.
Décommentez l'étape 4.4.3 du fichier HomeAppViewModel.kt
pour créer l'automatisation en appelant les API Home et en gérant les exceptions:
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
}
}
Exécutez maintenant l'application et observez les modifications sur votre appareil.
Une fois que vous avez sélectionné un déclencheur et une action, vous pouvez créer l'automatisation:
Assurez-vous d'attribuer un nom unique à votre automatisation, puis appuyez sur le bouton Créer une automatisation. Les API devraient être appelées et vous devriez être redirigé vers la vue de liste des automatisations avec votre automatisation:
Appuyez sur l'automatisation que vous venez de créer pour voir comment elle est renvoyée par les API.
Notez que l'API renvoie une valeur indiquant si une automatisation est valide et actuellement active ou non. Il est possible de créer des automatisations qui ne passent pas la validation lorsqu'elles sont analysées côté serveur. Si l'analyse d'une automatisation échoue, isValid
est défini sur false
, ce qui indique que l'automatisation n'est pas valide et inactive. Si votre automatisation n'est pas valide, consultez le champ automation.validationIssues
pour en savoir plus.
Assurez-vous que votre automatisation est définie comme valide et active, puis testez-la.
Tester votre automatisation
Les automatisations peuvent être exécutées de deux manières:
- Avec un événement de déclencheur. Si les conditions sont remplies, l'action que vous avez définie dans l'automatisation est déclenchée.
- Avec un appel d'API d'exécution manuelle.
Si un manualStarter()
est défini dans le bloc DSL de l'automatisation provisoire, le moteur d'automatisation prend en charge l'exécution manuelle de cette automatisation. Cela est déjà présent dans les exemples de code de l'application exemple.
Comme vous êtes toujours sur l'écran de la vue des automatisations de votre appareil mobile, appuyez sur le bouton Exécuter manuellement. Cela devrait appeler automation.execute()
, qui exécute votre commande d'action sur l'appareil que vous avez sélectionné lors de la configuration de l'automatisation.
Une fois que vous avez validé la commande d'action via une exécution manuelle à l'aide de l'API, il est temps de vérifier si elle s'exécute également à l'aide du déclencheur que vous avez défini.
Accédez à l'onglet "Devices" (Appareils), sélectionnez l'appareil d'action et le trait, puis définissez-le sur une valeur différente (par exemple, définissez LevelControl
(luminosité) de light2
sur 50%, comme illustré dans la capture d'écran suivante:
Nous allons maintenant essayer de déclencher l'automatisation à l'aide de l'appareil déclencheur. Sélectionnez le déclencheur que vous avez sélectionné lors de la création de l'automatisation. Activez/Désactivez le trait que vous avez choisi (par exemple, définissez OnOff
de starter outlet1
sur On
):
Vous constaterez que l'automatisation est également exécutée et que la valeur d'origine (100 %) est définie pour la caractéristique LevelControl
de l'appareil d'action light2
:
Félicitations, vous avez réussi à créer des automatisations à l'aide des API Home.
Pour en savoir plus sur l'API Automation, consultez la page API Android Automation.
5. Découvrir les fonctionnalités
Les API Home incluent une API dédiée appelée API Discovery, que les développeurs peuvent utiliser pour interroger les caractéristiques compatibles avec l'automatisation sur un appareil donné. L'application exemple montre comment utiliser cette API pour découvrir les commandes disponibles.
Commandes Discover
Dans cette section, nous allons voir comment découvrir les CommandCandidates
compatibles et comment créer une automatisation basée sur les nœuds candidats détectés.
Dans l'application exemple, nous appelons device.candidates()
pour obtenir une liste de candidats, qui peut inclure des instances de CommandCandidate
, EventCandidate
ou TraitAttributesCandidate
.
Accédez au fichier HomeAppViewModel.kt
et annulez la mise en commentaire de l'étape 5.1.1 pour récupérer la liste des candidats et filtrer avec le type 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)
}
Découvrez comment il filtre les CommandCandidate.
. Les candidats renvoyés par l'API appartiennent à différents types. L'application exemple est compatible avec CommandCandidate
. Décommentez l'étape 5.1.2 dans le commandMap
défini dans ActionViewModel.kt
pour définir ces traits compatibles:
// 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
)
Maintenant que nous pouvons appeler l'API Discovery et filtrer les résultats compatibles dans l'application exemple, nous allons voir comment les intégrer à notre éditeur.
Pour en savoir plus sur l'API Discovery, consultez Utiliser la détection d'appareils sur Android.
Intégrer l'éditeur
La méthode la plus courante consiste à les présenter à un utilisateur final pour qu'il puisse les sélectionner. Juste avant que l'utilisateur ne sélectionne les champs de l'automatisation de la version préliminaire, nous pouvons lui montrer la liste des actions détectées. En fonction de la valeur qu'il sélectionne, nous pouvons préremplir le nœud d'action dans la version préliminaire de l'automatisation.
Le fichier CandidatesView.kt
contient la classe de vue qui affiche les candidats détectés. Décomentez l'étape 5.2.1 pour activer la fonction .clickable{}
de CandidateListItem
, qui définit homeAppVM.selectedDraftVM
sur 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)) }
}) {
...
}
}
}
Comme pour l'étape 4.3 dans HomeAppView.kt
, lorsque selectedDraftVM
est défini, il affiche 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)
}
...
}
Réessayez en appuyant sur light2 - MOVE_TO_LEVEL (light2 - MOVE_TO_LEVEL), comme indiqué dans la section précédente. Vous êtes alors invité à créer une automatisation basée sur la commande du candidat:
Maintenant que vous savez créer des automatisations dans l'application exemple, vous pouvez les intégrer à vos applications.
6. Exemples d'automatisations avancées
Avant de conclure, nous allons examiner d'autres exemples de DSL d'automatisation. Ces exemples illustrent certaines des fonctionnalités avancées que vous pouvez obtenir avec les API.
Heure de la journée comme déclencheur
En plus des caractéristiques de l'appareil, les API Google Home proposent des caractéristiques basées sur la structure, telles que Time
. Vous pouvez créer une automatisation avec un déclencheur basé sur le temps, comme suit:
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
...
}
}
Diffusion de l'Assistant en tant qu'action
La caractéristique AssistantBroadcast
est disponible au niveau de l'appareil dans un SpeakerDevice
(si l'enceinte est compatible) ou au niveau de la structure (car les enceintes Google et les appareils mobiles Android peuvent diffuser des diffusions de l'Assistant). Exemple :
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!!")
)
}
}
}
Utiliser DelayFor
et suppressFor
L'API Automation fournit également des opérateurs avancés tels que delayFor, qui permet de retarder les commandes, et suppressFor, qui peut empêcher une automatisation d'être déclenchée par les mêmes événements pendant une période donnée. Voici quelques exemples d'utilisation de ces opérateurs:
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!"))
}
...
}
Utiliser AreaPresenceState
dans un déclencheur
AreaPresenceState
est un trait au niveau de la structure qui détecte si quelqu'un est à la maison.
Par exemple, l'exemple suivant montre comment verrouiller automatiquement les portes lorsqu'une personne est à la maison après 22h:
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()))
}
}
}
Maintenant que vous connaissez ces fonctionnalités d'automatisation avancées, créez des applications géniales !
7. Félicitations !
Félicitations ! Vous avez terminé la deuxième partie du développement d'une application Android à l'aide des API Google Home. Au cours de cet atelier de programmation, vous avez exploré les API Automation et Discovery.
Nous espérons que vous apprécierez de créer des applications qui contrôlent de manière créative les appareils de l'écosystème Google Home et de créer des scénarios d'automatisation passionnants à l'aide des API Home.
Étapes suivantes
- Consultez la section Dépannage pour découvrir comment déboguer efficacement les applications et résoudre les problèmes liés aux API Home.
- Vous pouvez nous contacter pour nous faire part de vos recommandations ou signaler un problème via l'outil de suivi des problèmes, dans la section d'assistance Smart Home.