1. Avant de commencer
Il s'agit du deuxième atelier de programmation d'une série sur la création d'une application Android à l'aide des API Google Home. Dans cet atelier de programmation, nous allons vous expliquer comment créer des automatisations pour la maison et vous donner quelques conseils sur les bonnes pratiques d'utilisation des API. Si vous n'avez pas encore suivi 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 pourront 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 prennent en charge.
Prérequis
- Vous avez suivi l'atelier de programmation Créer une application mobile à l'aide des API Home sur Android.
- Connaissance de l'écosystème Google Home (cloud à cloud et Matter).
- Un poste de travail sur lequel est installé Android Studio (2024.3.1 Ladybug ou version ultérieure).
- Un téléphone Android qui répond aux exigences des API Home (consultez Conditions préalables), avec les services Google Play et l'application Google Home installés. Un émulateur ne fonctionnera pas. Seuls les téléphones Android physiques sont compatibles avec l'application exemple.
- Un Google Home Hub compatible avec les API Google Home.
- Facultatif : un appareil connecté compatible avec les API Google Home.
Points abordés
- Découvrez comment créer des automatisations pour les appareils connectés à l'aide des API Home.
- Découvrez comment utiliser les API Discovery pour explorer les fonctionnalités de l'appareil compatibles.
- Découvrez comment appliquer 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 Home APIs :
- 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 dans GMSCore pour contrôler les appareils connectés. 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 et permet d'activer des fonctionnalités clés sur tous les appareils Android certifiés. Le module Home 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 d'avoir configuré et fait fonctionner une structure avec au moins deux appareils compatibles sur le compte. Comme nous allons configurer des automatisations dans cet atelier de programmation (un changement d'é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 et 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 de la codebase s'appuie sur 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 terminer cet atelier de programmation au lieu d'utiliser codelab-branch-2
.
Une fois le code source compilé et prêt à être exécuté sur votre appareil mobile, passez à la section suivante.
3. En savoir plus sur les automatisations
Les automatisations sont un ensemble d'instructions "si ceci, alors cela" qui peuvent contrôler l'état des appareils de manière automatisée en fonction des 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 nœuds : 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 :
- Déclencheur : définit les conditions initiales qui activent l'automatisation, comme une modification de la valeur d'un trait. Une automatisation doit comporter un déclencheur.
- 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 effectuées lorsque toutes les conditions sont remplies.
Par exemple, vous pouvez créer une automatisation qui baisse l'intensité des lumières d'une pièce lorsqu'un interrupteur est actionné et que le téléviseur de cette pièce est allumé. Dans cet exemple :
- Starter : le bouton de la pièce est activé.
- Condition : l'état OnOff du téléviseur est défini sur "On".
- Action : les lumières de la pièce où se trouve le Switch sont tamisé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 s'exécutant simultanément, comme l'allumage de plusieurs lumières en même temps. Les nœuds suivant un flux parallèle ne s'exécutent pas tant que toutes les branches du flux parallèle ne sont pas terminées.
Il existe d'autres types de nœuds dans le schéma d'automatisation. 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.
Explorer 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 Protocol Buffer, qui sont 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 élémentaire, vous pouvez coder explicitement une automatisation pour utiliser 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'automatisation
Dans l'application exemple, nous allons créer un éditeur d'automatisation permettant aux utilisateurs de sélectionner des appareils, les fonctionnalités (actions) qu'ils souhaitent utiliser et la façon 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 lance 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 a besoin des éléments suivants :
- Appareil
- Trait
- Opération
- Valeur
L'appareil et le trait 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 traités séparément.
L'application définit une liste prédéfinie d'opérations :
// 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
}
Ensuite, pour chaque caractéristique compatible, il effectue le suivi des 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 la même manière, l'application exemple effectue le suivi des valeurs attribuables aux caractéristiques :
enum class OnOffValue {
On,
Off,
}
enum class ThermostatValue {
Heat,
Cool,
Off,
}
Il assure également le suivi d'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 requis.
Décommentez l'étape 4.1.1 dans le 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
// }
// )
// }
}
Annulez la mise en commentaire de 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émenter 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 du fichier StarterView.kt
pour stocker toutes les variables de démarrage ViewModel
dans le ViewModel
de démarrage de l'automatisation brouillon (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)) }
L'exécution de l'application et la sélection d'une nouvelle automatisation et d'un nouveau starter devraient afficher une vue semblable à celle-ci :
L'application exemple n'est compatible qu'avec les starters 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 la façon dont elle modifie 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 Home APIs 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, telle que MoveToLevel()
et un pourcentage cible.
L'appareil et le trait peuvent être sélectionnés parmi les objets renvoyés par l'API Devices.
L'application définit une liste prédéfinie de commandes :
// List of operations available when creating automation starters:
enum class Action {
ON,
OFF,
MOVE_TO_LEVEL,
MODE_HEAT,
MODE_COOL,
MODE_OFF,
}
L'application effectue le suivi des opérations acceptées pour chaque trait accepté :
// 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 requis.
Décommentez l'étape 4.2.1 dans le fichier ActionView.kt
pour afficher tous les appareils d'action et implémenter 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émenter le rappel de clic dans un DropdownMenu
afin de définir le actionTrait
, qui représente le trait auquel appartient la commande.
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 du fichier ActionView.kt
pour afficher toutes les actions disponibles de actionTrait
et implémenter le rappel de clic dans un DropdownMenu
pour définir 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
// }
// )
// }
}
Décommentez l'étape 4.2.4 dans le fichier ActionView.kt
pour afficher les valeurs disponibles de l'action (commande) du trait et stocker la valeur dans actionValueLevel
dans le rappel de changement 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
de l'automatisation brouillon (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 vous exécutez l'application et sélectionnez une nouvelle automatisation et une nouvelle action, une vue semblable à celle-ci devrait s'afficher :
Nous n'acceptons que les actions basées sur les caractéristiques de l'appareil dans l'application exemple.
Afficher un brouillon d'automatisation
Une fois le DraftViewModel
terminé, il peut être affiché 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 avez appris à créer des déclencheurs et des actions, vous êtes prêt à créer un brouillon d'automatisation et à l'envoyer à l'API Automations. 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'automatisation du brouillon 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 le brouillon d'automatisation à l'aide des variables de démarrage et d'action de la section précédente.
Décommentez 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émarrage 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 du fichier DraftViewModel.kt
pour créer les expressions parallèles nécessaires à la création du 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 pour finaliser une automatisation consiste à implémenter la fonction getDraftAutomation
afin de créer un AutomationDraft.
.
Décommentez l'étape 4.4.3 dans le 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 changements sur votre appareil.
Une fois que vous avez sélectionné un déclencheur et une action, vous pouvez créer l'automatisation :
Assurez-vous de donner 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 Liste des automatisations avec votre automatisation :
Appuyez sur l'automatisation que vous venez de créer et voyez comment elle est renvoyée par les API.
Sachez que l'API renvoie une valeur indiquant si une automatisation est valide et actuellement active. Il est possible de créer des automatisations qui ne sont pas validées lorsqu'elles sont analysées côté serveur. Si l'analyse d'une automatisation échoue lors de la validation, isValid
est défini sur false
, ce qui indique que l'automatisation n'est pas valide et est 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 correspondent, l'action que vous avez définie dans l'automatisation est déclenchée.
- Avec un appel d'API d'exécution manuelle.
Si une automatisation brouillon comporte un manualStarter()
défini dans le bloc DSL du brouillon d'automatisation, le moteur d'automatisation prendra en charge l'exécution manuelle de cette automatisation. Cette fonctionnalité est déjà présente dans les exemples de code de l'application exemple.
Comme vous êtes toujours sur l'écran de vue de l'automatisation sur 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 par 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 "Appareils", sélectionnez l'appareil d'action et la caractéristique, puis définissez-la sur une autre valeur (par exemple, définissez la 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 de démarrage. Choisissez l'appareil déclencheur que vous avez sélectionné lors de la création de l'automatisation. Activez le trait que vous avez choisi (par exemple, définissez OnOff
de starter outlet1
sur On
) :
Vous verrez que cela exécute également l'automatisation et définit le trait LevelControl
de l'appareil d'action light2
sur la valeur d'origine, à savoir 100 % :
Félicitations, vous avez réussi à créer des automatisations à l'aide des API Home !
Pour en savoir plus sur l'API Automation, consultez API Android Automation.
5. Découvrir les fonctionnalités
Les API Home incluent une API dédiée appelée Discovery API, que les développeurs peuvent utiliser pour déterminer les traits compatibles avec l'automatisation qui sont pris en charge par un appareil donné. L'application exemple fournit un exemple d'utilisation de cette API pour découvrir les commandes disponibles.
Découvrir les commandes
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écouverts.
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 que nous prenons en charge dans l'application exemple, nous allons voir comment l'intégrer à notre éditeur.
Pour en savoir plus sur l'API Discovery, consultez Exploiter la découverte d'appareils sur Android.
Intégrer l'éditeur
La façon la plus courante d'utiliser les actions détectées 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 brouillon, 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 le brouillon d'automatisation.
Le fichier CandidatesView.kt
contient la classe d'affichage qui présente les candidats découverts. Décommentez l'étape 5.2.1 pour activer la fonction .clickable{}
de CandidateListItem
, qui définit homeAppVM.selectedDraftVM
comme 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 à l'étape 4.3 de 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, comme indiqué dans la section précédente. Vous serez alors invité à créer une automatisation basée sur la commande du candidat :
Maintenant que vous savez comment créer des automatisations dans l'application exemple, vous pouvez les intégrer à vos applications.
6. Exemples d'automatisation avancée
Avant de conclure, nous allons examiner d'autres exemples de DSL d'automatisation. Elles 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 traits d'appareil, les API Google Home proposent des traits basés sur la structure, tels 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
Le trait AssistantBroadcast
est disponible en tant que trait au niveau de l'appareil dans un SpeakerDevice
(si l'enceinte le prend en charge) ou en tant que trait au niveau de la structure (car les enceintes Google et les appareils mobiles Android peuvent lire les 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 Automations fournit également des opérateurs avancés tels que delayFor, qui permet de retarder les commandes, et suppressFor, qui permet d'empêcher le déclenchement d'un automatisme 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, lancez-vous et 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. Tout au long 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 concevoir des scénarios d'automatisation intéressants à 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 des problèmes via l'outil de suivi des problèmes, rubrique d'aide sur la maison connectée.