1. Zanim zaczniesz
To drugie ćwiczenia z programowania z serii poświęconej tworzeniu aplikacji na Androida za pomocą interfejsów Google Home API. W tych ćwiczeniach z programowania pokażemy, jak tworzyć automatyzacje domowe, i podamy kilka wskazówek dotyczących sprawdzonych metod korzystania z interfejsów API. Jeśli nie masz jeszcze za sobą pierwszego laboratorium programowania, Tworzenie aplikacji mobilnej za pomocą interfejsów Home API na Androidzie, zalecamy, aby przed rozpoczęciem tego laboratorium programowania wykonać to pierwsze.
Interfejsy Google Home API to zestaw bibliotek dla programistów na Androida, które umożliwiają sterowanie inteligentnymi urządzeniami domowymi w ekosystemie Google Home. Dzięki tym nowym interfejsom API deweloperzy będą mogli konfigurować automatyzację inteligentnego domu, która będzie sterować funkcjami urządzeń na podstawie zdefiniowanych warunków. Google udostępnia też interfejs Discovery API, który umożliwia wysyłanie zapytań do urządzeń w celu sprawdzenia, jakie atrybuty i polecenia obsługują.
Wymagania wstępne
- Wykonaj ćwiczenia z tego kursu (w języku angielskim), aby dowiedzieć się, jak tworzyć aplikacje mobilne przy użyciu interfejsów Home API na Androidzie.
- Znajomość ekosystemu Google Home (Cloud-to-cloud i Matter).
- Stacja robocza z zainstalowanym Androidem Studio (wersja 2024.3.1 Ladybug lub nowsza).
- Telefon z Androidem spełniający wymagania interfejsów Home API (patrz Wymagania wstępne) z zainstalowanymi Usługami Google Play i aplikacją Google Home. Emulator nie będzie działać. Przykładowa aplikacja jest obsługiwana tylko na fizycznych telefonach z Androidem.
- Kompatybilny Google Home Hub, który obsługuje interfejsy Google Home API.
- Opcjonalnie: inteligentne urządzenie domowe zgodne z interfejsami Google Home API.
Czego się nauczysz
- Jak tworzyć automatyzacje do inteligentnych urządzeń domowych za pomocą interfejsów Home API.
- Jak korzystać z interfejsów Discovery API, aby poznać obsługiwane funkcje urządzenia.
- Jak stosować sprawdzone metody podczas tworzenia aplikacji za pomocą interfejsów Home API.
2. Konfiguruję projekt
Ten diagram ilustruje architekturę aplikacji korzystającej z interfejsów Home API:
- Kod aplikacji: podstawowy kod, nad którym pracują deweloperzy, aby stworzyć interfejs użytkownika aplikacji i logikę interakcji z pakietem SDK interfejsów API Home.
- Pakiet SDK interfejsów Home API: pakiet SDK interfejsów Home API udostępniany przez Google współpracuje z usługą Home API w GMSCore, aby sterować urządzeniami inteligentnego domu. Deweloperzy tworzą aplikacje współpracujące z interfejsami Home API, łącząc je z pakietem SDK interfejsów Home API.
- GMSCore na Androidzie: GMSCore, czyli Usługi Google Play, to platforma Google, która zapewnia podstawowe usługi systemowe, umożliwiając działanie najważniejszych funkcji na wszystkich certyfikowanych urządzeniach z Androidem. Moduł domowy usług Google Play zawiera usługi, które wchodzą w interakcję z interfejsami Home API.
W tym ćwiczeniu z programowania wykorzystamy wiedzę zdobytą w artykule Tworzenie aplikacji mobilnej z użyciem interfejsów Home API na Androidzie.
Upewnij się, że masz skonfigurowaną i działającą na koncie strukturę z co najmniej 2 obsługiwanymi urządzeniami. W tym laboratorium będziemy konfigurować automatyzację (zmiana stanu urządzenia wywołuje działanie na innym urządzeniu), więc do sprawdzenia wyników potrzebne będą 2 urządzenia.
Pobieranie przykładowej aplikacji
Kod źródłowy przykładowej aplikacji jest dostępny na GitHubie w repozytorium google-home/google-home-api-sample-app-android.
W tym laboratorium kodu używane są przykłady z gałęzi codelab-branch-2
aplikacji przykładowej.
Przejdź do miejsca, w którym chcesz zapisać projekt, i sklonuj gałąź codelab-branch-2
:
$ git clone -b codelab-branch-2 https://github.com/google-home/google-home-api-sample-app-android.git
Pamiętaj, że jest to inna gałąź niż ta, która jest używana w artykule Tworzenie aplikacji mobilnej za pomocą interfejsów Home API na Androidzie. Ta gałąź bazy kodu jest kontynuacją pierwszego laboratorium. Tym razem przykłady pokazują, jak tworzyć automatyzacje. Jeśli udało Ci się ukończyć poprzednie ćwiczenia z programowania i wszystkie funkcje działają prawidłowo, możesz użyć tego samego projektu Android Studio, aby ukończyć te ćwiczenia, zamiast korzystać z codelab-branch-2
.
Gdy kod źródłowy zostanie skompilowany i będzie gotowy do uruchomienia na urządzeniu mobilnym, przejdź do następnej sekcji.
3. Więcej informacji o automatyzacji
Automatyzacje to zestaw instrukcji „jeśli to, to tamto”, które mogą automatycznie kontrolować stany urządzeń na podstawie wybranych czynników. Deweloperzy mogą używać automatyzacji do tworzenia zaawansowanych funkcji interaktywnych w swoich interfejsach API.
Automatyzacje składają się z 3 rodzajów komponentów zwanych węzłami: poleceń inicjujących, działań i warunków. Te węzły współpracują ze sobą, aby automatyzować zachowania za pomocą inteligentnych urządzeń domowych. Zazwyczaj są one oceniane w tej kolejności:
- Polecenie inicjujące – określa warunki początkowe, które aktywują automatyzację, np. zmianę wartości atrybutu. Automatyzacja musi mieć polecenie inicjujące.
- Warunek – dodatkowe ograniczenia do oceny po uruchomieniu automatyzacji. Wyrażenie w warunku musi zwracać wartość „prawda”, aby można było wykonać działania automatyzacji.
- Działanie – polecenia lub aktualizacje stanu, które są wykonywane po spełnieniu wszystkich warunków.
Możesz na przykład ustawić automatyzację, która przyciemnia światła w pomieszczeniu, gdy włącznik jest przełączony, a telewizor w tym pomieszczeniu jest włączony. W tym przykładzie:
- Starter – przełącznik w pokoju jest przełączony.
- Warunek – stan włączenia/wyłączenia telewizora jest oceniany jako włączony.
- Działanie – światła w pomieszczeniu, w którym znajduje się Switch, zostaną przyciemnione.
Te węzły są oceniane przez silnik automatyzacji szeregowo lub równolegle.
Przepływ sekwencyjny zawiera węzły, które są wykonywane w kolejności sekwencyjnej. Zwykle są to polecenie inicjujące, warunek i działanie.
Przepływ równoległy może mieć wiele węzłów działania wykonywanych jednocześnie, np. włączanie kilku świateł w tym samym czasie. Węzły w przepływie równoległym nie zostaną wykonane, dopóki nie zostaną ukończone wszystkie gałęzie tego przepływu.
W schemacie automatyzacji występują też inne typy węzłów. Więcej informacji o nich znajdziesz w sekcji Węzły w Przewodniku dla deweloperów dotyczącym interfejsów Home API. Deweloperzy mogą też łączyć różne typy węzłów, aby tworzyć złożone automatyzacje, takie jak:
Deweloperzy udostępniają te węzły silnikowi automatyzacji za pomocą języka DSL stworzonego specjalnie na potrzeby automatyzacji w Google Home.
Poznaj język DSL automatyzacji
Język specyficzny dla domeny (DSL) to język używany do rejestrowania zachowania systemu w kodzie. Kompilator generuje klasy danych, które są serializowane do formatu JSON bufora protokołu i używane do wywoływania usług automatyzacji Google.
DSL wyszukuje ten schemat:
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()) }
}
}
Automatyzacja w poprzednim przykładzie synchronizuje 2 żarówki. Gdy stan OnOff
urządzenia device1
zmieni się na On
(onOffTrait.onOff equals true
), stan OnOff
urządzenia device2
zmieni się na On
(command(OnOff.on()
).
Pamiętaj, że podczas pracy z automatyzacjami obowiązują limity zasobów.
Automatyzacje to bardzo przydatne narzędzie do tworzenia zautomatyzowanych funkcji w inteligentnym domu. W najprostszym przypadku możesz jawnie zaprogramować automatyzację, aby używała określonych urządzeń i cech. Bardziej praktycznym zastosowaniem jest jednak sytuacja, w której aplikacja umożliwia użytkownikowi konfigurowanie urządzeń, poleceń i parametrów automatyzacji. W następnej sekcji dowiesz się, jak utworzyć edytor automatyzacji, który umożliwi użytkownikowi wykonanie tych czynności.
4. Tworzenie edytora automatyzacji
W aplikacji przykładowej utworzymy edytor automatyzacji, w którym użytkownicy będą mogli wybierać urządzenia, funkcje (działania), których chcą używać, oraz sposób wywoływania automatyzacji za pomocą starterów.
Konfigurowanie początków
Element uruchamiający automatyzację to punkt wejścia do automatyzacji. Polecenie inicjujące uruchamia automatyzację, gdy nastąpi określone zdarzenie. W aplikacji przykładowej rejestrujemy elementy inicjujące automatyzację za pomocą klasy StarterViewModel
, która znajduje się w pliku źródłowym StarterViewModel.kt
, i wyświetlamy widok edytora za pomocą funkcji StarterView
(StarterView.kt
).
Węzeł początkowy musi zawierać te elementy:
- Urządzenie
- Cechy
- Operacja
- Wartość
Urządzenie i cechę można wybrać z obiektów zwróconych przez interfejs Devices API. Polecenia i parametry dla każdego obsługiwanego urządzenia to bardziej złożona kwestia, którą należy rozpatrywać oddzielnie.
Aplikacja definiuje wstępnie ustawioną listę operacji:
// 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
}
Następnie dla każdego obsługiwanego atrybutu śledzi obsługiwane operacje:
// 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
))
Podobnie aplikacja przykładowa śledzi wartości, które można przypisać do cech:
enum class OnOffValue {
On,
Off,
}
enum class ThermostatValue {
Heat,
Cool,
Off,
}
Śledzi też mapowanie między wartościami zdefiniowanymi przez aplikację a wartościami zdefiniowanymi przez interfejsy 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,
)
Aplikacja wyświetla zestaw elementów widoku, za pomocą których użytkownicy mogą wybrać wymagane pola.
Odkomentuj krok 4.1.1 w pliku StarterView.kt
, aby wyrenderować wszystkie urządzenia startowe i zastosować wywołanie zwrotne kliknięcia w pliku 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
// }
// )
// }
}
Odkomentuj krok 4.1.2 w pliku StarterView.kt
, aby wyrenderować wszystkie cechy urządzenia startowego i zastosować wywołanie zwrotne kliknięcia w pliku 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
// }
// )
}
}
Odkomentuj krok 4.1.3 w pliku StarterView.kt
, aby wyrenderować wszystkie operacje wybranego atrybutu i zastosować wywołanie zwrotne kliknięcia w pliku 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
// }
// )
// }
}
Odkomentuj krok 4.1.4 w pliku StarterView.kt
, aby renderować wszystkie wartości wybranego atrybutu i wdrożyć wywołanie zwrotne kliknięcia w pliku 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 -> {
...
}
}
Odkomentuj krok 4.1.5 w pliku StarterView.kt
, aby zapisać wszystkie zmienne polecenia inicjującego ViewModel
w zmiennej polecenia inicjującego ViewModel
automatyzacji wersji roboczej (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)) }
Uruchomienie aplikacji i wybranie nowej automatyzacji oraz elementu początkowego powinno spowodować wyświetlenie widoku podobnego do tego:
Aplikacja przykładowa obsługuje tylko elementy początkowe oparte na cechach urządzenia.
Konfigurowanie działań
Działanie automatyzacji odzwierciedla główny cel automatyzacji, czyli sposób, w jaki zmienia ona świat fizyczny. W aplikacji przykładowej rejestrujemy działania automatyzacji za pomocą klasy ActionViewModel
, a widok edytora wyświetlamy za pomocą klasy ActionView
.
Aplikacja przykładowa używa tych elementów interfejsów Home API do definiowania węzłów działań automatyzacji:
- Urządzenie
- Cechy
- Polecenie
- Wartość (opcjonalnie)
Każde działanie polecenia urządzenia używa polecenia, ale niektóre wymagają też powiązanej z nim wartości parametru, np. MoveToLevel()
i docelowego odsetka.
Urządzenie i cechę można wybrać z obiektów zwróconych przez interfejs Devices API.
Aplikacja definiuje wstępnie zdefiniowaną listę poleceń:
// List of operations available when creating automation starters:
enum class Action {
ON,
OFF,
MOVE_TO_LEVEL,
MODE_HEAT,
MODE_COOL,
MODE_OFF,
}
Aplikacja śledzi obsługiwane operacje dla każdej obsługiwanej cechy:
// 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,
)
W przypadku poleceń, które przyjmują co najmniej 1 parametr, istnieje też zmienna:
val valueLevel: MutableStateFlow<UByte?>
Interfejs API wyświetla zestaw elementów widoku, za pomocą których użytkownicy mogą wybierać wymagane pola.
Odkomentuj krok 4.2.1 w pliku ActionView.kt
, aby wyrenderować wszystkie urządzenia do obsługi działań i zastosować wywołanie zwrotne kliknięcia w pliku DropdownMenu
, aby ustawić 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
// }
// )
// }
}
Odkomentuj krok 4.2.2 w pliku ActionView.kt
, aby wyrenderować wszystkie cechy urządzenia actionDeviceVM
, i zastosuj wywołanie zwrotne kliknięcia w elemencie DropdownMenu
, aby ustawić parametr actionTrait
, który reprezentuje cechę, do której należy polecenie.
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
// }
// )
// }
}
Odkomentuj krok 4.2.3 w pliku ActionView.kt
, aby wyrenderować wszystkie dostępne działania actionTrait
, i zastosuj wywołanie zwrotne kliknięcia w pliku DropdownMenu
, aby ustawić actionAction
, który reprezentuje wybrane działanie automatyzacji.
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
// }
// )
// }
}
Odkomentuj krok 4.2.4 w pliku ActionView.kt
, aby wyrenderować dostępne wartości działania cechy (polecenia) i zapisać wartość w actionValueLevel
w wywołaniu zwrotnym zmiany wartości:
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
// )
// }
...
}
Odkomentuj krok 4.2.5 w pliku ActionView.kt
, aby zapisać wszystkie zmienne działania ViewModel
w działaniu ViewModel
wersji roboczej automatyzacji (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)) }
Uruchomienie aplikacji i wybranie nowej automatyzacji oraz działania powinno spowodować wyświetlenie widoku podobnego do tego:
W aplikacji przykładowej obsługujemy tylko działania oparte na cechach urządzenia.
Renderowanie wersji roboczej automatyzacji
Gdy DraftViewModel
zostanie ukończone, może być renderowane przez HomeAppView.kt
:
fun HomeAppView (homeAppVM: HomeAppViewModel) {
...
// If a draft automation is selected, show the draft editor:
if (selectedDraftVM != null) {
DraftView(homeAppVM)
}
...
}
W DraftView.kt
:
fun DraftView (homeAppVM: HomeAppViewModel) {
val draftVM: DraftViewModel = homeAppVM.selectedDraftVM.collectAsState().value!!
...
// Draft Starters:
DraftStarterList(draftVM)
// Draft Actions:
DraftActionList(draftVM)
}
Utwórz automatyzację
Teraz, gdy wiesz już, jak tworzyć elementy początkowe i działania, możesz utworzyć wersję roboczą automatyzacji i wysłać ją do interfejsu Automation API. Interfejs API ma funkcję createAutomation()
, która przyjmuje wersję roboczą automatyzacji jako argument i zwraca nową instancję automatyzacji.
Przygotowanie wersji roboczej automatyzacji odbywa się w klasie DraftViewModel
w aplikacji przykładowej. Więcej informacji o tym, jak tworzymy wersję roboczą automatyzacji za pomocą zmiennych początkowych i zmiennych działania z poprzedniej sekcji, znajdziesz w funkcji getDraftAutomation()
.
Odkomentuj krok 4.4.1 w pliku DraftViewModel.kt
, aby utworzyć wyrażenia „select” wymagane do utworzenia wykresu automatyzacji, gdy cecha początkowa to 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()
...
}
Odkomentuj krok 4.4.2 w pliku DraftViewModel.kt
, aby utworzyć wyrażenia równoległe wymagane do utworzenia wykresu automatyzacji, gdy wybranym atrybutem działania jest LevelControl
, a wybranym działaniem jest 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) }
...
}
Ostatnim krokiem tworzenia automatyzacji jest wdrożenie funkcji getDraftAutomation
, aby utworzyć AutomationDraft.
.
Odkomentuj krok 4.4.3 w pliku HomeAppViewModel.kt
, aby utworzyć automatyzację przez wywołanie interfejsów Home API i obsługę wyjątków:
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
}
}
Uruchom teraz aplikację i sprawdź zmiany na urządzeniu.
Po wybraniu polecenia inicjującego i czynności możesz utworzyć automatyzację:
Nadaj automatyzacji unikalną nazwę, a następnie kliknij przycisk Utwórz automatyzację. Spowoduje to wywołanie interfejsów API i powrót do widoku listy automatyzacji z Twoją automatyzacją:
Kliknij utworzoną automatyzację i sprawdź, jak jest zwracana przez interfejsy API.
Pamiętaj, że interfejs API zwraca wartość wskazującą, czy automatyzacja jest prawidłowa i obecnie aktywna. Możesz tworzyć automatyzacje, które nie przechodzą weryfikacji podczas analizowania po stronie serwera. Jeśli analiza automatyzacji nie przejdzie weryfikacji, wartość isValid
zostanie ustawiona na false
, co oznacza, że automatyzacja jest nieprawidłowa i nieaktywna. Jeśli automatyzacja jest nieprawidłowa, sprawdź pole automation.validationIssues
, aby uzyskać więcej informacji.
Upewnij się, że automatyzacja jest ustawiona jako prawidłowa i aktywna, a następnie możesz ją wypróbować.
Wypróbuj automatyzację
Automatyzacje można uruchamiać na 2 sposoby:
- Za pomocą zdarzenia inicjującego. Jeśli warunki są spełnione, uruchamia to działanie skonfigurowane w automatyzacji.
- za pomocą wywołania interfejsu API ręcznego wykonywania.
Jeśli w bloku DSL wersji roboczej automatyzacji zdefiniowano manualStarter()
, silnik automatyzacji będzie obsługiwać ręczne wykonywanie tej automatyzacji. Jest to już obecne w przykładach kodu w aplikacji przykładowej.
Ponieważ nadal jesteś na ekranie widoku automatyzacji na urządzeniu mobilnym, kliknij przycisk Ręczne wykonanie. Powinno to wywołać funkcję automation.execute()
, która uruchamia polecenie działania na urządzeniu wybranym podczas konfigurowania automatyzacji.
Po sprawdzeniu polecenia działania przez ręczne wykonanie za pomocą interfejsu API możesz sprawdzić, czy jest ono wykonywane również za pomocą zdefiniowanego przez Ciebie narzędzia.
Otwórz kartę Urządzenia, wybierz urządzenie i cechę, a następnie ustaw inną wartość (np. ustaw light2
LevelControl
(jasność) na 50%, jak pokazano na zrzucie ekranu poniżej):
Teraz spróbujemy uruchomić automatyzację za pomocą urządzenia początkowego. Wybierz urządzenie inicjujące, które zostało wybrane podczas tworzenia automatyzacji. Przełącz wybraną cechę (np. ustaw starter outlet1
OnOff
na On
):
Zauważysz, że automatyzacja została wykonana, a cecha LevelControl
urządzenia wykonującego działanie light2
została ustawiona na pierwotną wartość 100%:
Gratulacje! Udało Ci się utworzyć automatyzacje za pomocą interfejsów Home API.
Więcej informacji o interfejsie Automation API znajdziesz w artykule Android Automation API.
5. Odkrywanie możliwości
Interfejsy Home API obejmują specjalny interfejs Discovery API, którego programiści mogą używać do sprawdzania, które funkcje automatyzacji są obsługiwane na danym urządzeniu. Przykładowa aplikacja zawiera przykład użycia tego interfejsu API do odkrywania dostępnych poleceń.
Odkrywanie poleceń
W tej sekcji omawiamy, jak wykrywać obsługiwane CommandCandidates
i jak tworzyć automatyzację na podstawie wykrytych węzłów kandydatów.
W aplikacji przykładowej wywołujemy funkcję device.candidates()
, aby uzyskać listę kandydatów, która może zawierać instancje CommandCandidate
, EventCandidate
lub TraitAttributesCandidate
.
Otwórz plik HomeAppViewModel.kt
i usuń znacznik komentarza z kroku 5.1.1, aby pobrać listę kandydatów i odfiltrować ją według typu 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)
}
Zobacz, jak filtruje CommandCandidate.
Kandydaci zwracani przez interfejs API należą do różnych typów. Przykładowa aplikacja obsługuje CommandCandidate
. Odkomentuj krok 5.1.2 w commandMap
zdefiniowanym w ActionViewModel.kt
, aby ustawić te obsługiwane cechy:
// 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
)
Teraz, gdy możemy wywoływać interfejs Discovery API i filtrować wyniki, które obsługujemy w aplikacji przykładowej, omówimy, jak możemy zintegrować to z naszym edytorem.
Więcej informacji o interfejsie Discovery API znajdziesz w artykule Wykorzystywanie wykrywania urządzeń na Androidzie.
Integrowanie edytora
Najczęstszym sposobem wykorzystania wykrytych działań jest wyświetlanie ich użytkownikowi, aby mógł je wybrać. Tuż przed wybraniem przez użytkownika pól wersji roboczej automatyzacji możemy wyświetlić listę wykrytych działań. W zależności od wybranej wartości możemy wstępnie wypełnić węzeł działania w wersji roboczej automatyzacji.
Plik CandidatesView.kt
zawiera klasę widoku, która wyświetla wykryte kandydatury. Usuń komentarz z kroku 5.2.1, aby włączyć funkcję .clickable{}
w CandidateListItem
, która ustawia homeAppVM.selectedDraftVM
jako 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)) }
}) {
...
}
}
}
Podobnie jak w kroku 4.3 w HomeAppView.kt
, gdy ustawiona jest wartość selectedDraftVM
, renderuje ona 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)
}
...
}
Spróbuj ponownie, klikając light2 - MOVE_TO_LEVEL (pokazane w poprzedniej sekcji), co spowoduje utworzenie nowej automatyzacji na podstawie polecenia kandydata:
Teraz, gdy znasz już proces tworzenia automatyzacji w aplikacji przykładowej, możesz zintegrować automatyzacje ze swoimi aplikacjami.
6. Przykłady zaawansowanej automatyzacji
Na koniec omówimy jeszcze kilka przykładów DSL automatyzacji. Pokazują one niektóre zaawansowane funkcje, które możesz uzyskać dzięki interfejsom API.
Pora dnia jako element wyzwalający
Oprócz cech urządzenia interfejsy Google Home API oferują cechy oparte na strukturze, takie jak Time
. Możesz utworzyć automatyzację z aktywatorami opartymi na czasie, np. taką:
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
...
}
}
Przesyłanie wiadomości przez Asystenta jako działanie
Cechę AssistantBroadcast
można uzyskać jako cechę na poziomie urządzenia w SpeakerDevice
(jeśli głośnik ją obsługuje) lub jako cechę na poziomie struktury (ponieważ głośniki Google i urządzenia mobilne z Androidem mogą odtwarzać transmisje Asystenta). Na przykład:
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!!")
)
}
}
}
Użyj DelayFor
i suppressFor
Interfejs Automation API udostępnia też zaawansowane operatory, takie jak delayFor, który służy do opóźniania poleceń, oraz suppressFor, który może zapobiegać wywoływaniu automatyzacji przez te same zdarzenia w określonym przedziale czasu. Oto kilka przykładów użycia tych operatorów:
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!"))
}
...
}
Używanie AreaPresenceState
w poleceniu inicjującym
AreaPresenceState
to cecha na poziomie struktury, która wykrywa, czy ktoś jest w domu.
Na przykład poniższy przykład pokazuje automatyczne blokowanie drzwi, gdy ktoś jest w domu po godzinie 22:00:
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()))
}
}
}
Teraz, gdy znasz już te zaawansowane funkcje automatyzacji, możesz tworzyć wspaniałe aplikacje.
7. Gratulacje!
Gratulacje! Udało Ci się ukończyć drugą część tworzenia aplikacji na Androida przy użyciu interfejsów Google Home API. W tym ćwiczeniu z programowania poznaliśmy interfejsy Automation API i Discovery API.
Mamy nadzieję, że tworzenie aplikacji, które w kreatywny sposób sterują urządzeniami w ekosystemie Google Home i umożliwiają tworzenie ciekawych scenariuszy automatyzacji za pomocą interfejsów Home API, będzie dla Ciebie przyjemnością.
Dalsze kroki
- Więcej informacji o skutecznym debugowaniu aplikacji i rozwiązywaniu problemów z interfejsami Home API znajdziesz w sekcji Rozwiązywanie problemów.
- Możesz przesłać nam rekomendacje lub zgłosić problemy za pomocą narzędzia Issue Tracker w temacie pomocy dotyczącej inteligentnego domu.