1. Zanim zaczniesz
To drugie z cyklu ćwiczeń z programowania dotyczące tworzenia aplikacji na Androida przy użyciu interfejsów API Google Home. W tym ćwiczeniu z programowania omawiamy tworzenie automatyzacji domowej i podajemy kilka wskazówek dotyczących sprawdzonych metod korzystania z interfejsów API. Jeśli nie ukończyłeś(-aś) jeszcze pierwszego ćwiczenia, Utwórz aplikację mobilną z użyciem interfejsów Home API na Androidzie, zalecamy ukończenie go przed rozpoczęciem tego ćwiczenia.
Interfejsy API Google Home udostępniają zestaw bibliotek programistom Androida, aby mogli sterować inteligentnymi urządzeniami domowymi w ekosystemie Google Home. Dzięki tym nowym interfejsom API deweloperzy będą mogli konfigurować automatyzacje dla inteligentnego domu, które mogą sterować funkcjami urządzenia na podstawie wstępnie zdefiniowanych warunków. Google udostępnia też interfejs Discovery API, który umożliwia wysyłanie zapytań do urządzeń w celu uzyskania informacji o tym, jakie atrybuty i polecenia obsługują.
Wymagania wstępne
- Ukończ kurs Tworzenie aplikacji mobilnej przy użyciu interfejsów Home API na Androidzie.
- Znajomość ekosystemu Google Home (chmura–chmura i Matter).
- Stacja robocza z zainstalowaną aplikacją Android Studio (2024.3.1 Ladybug lub nowszą).
- telefon z Androidem, który spełnia wymagania interfejsów API Home (patrz Wymagania wstępne), z zainstalowanymi Usługami Google Play i aplikacją Google Home.
- zgodny 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 dla urządzeń inteligentnego domu za pomocą interfejsów API Home.
- Jak korzystać z interfejsów Discovery API, aby poznać obsługiwane funkcje urządzenia.
- Jak stosować sprawdzone metody podczas tworzenia aplikacji z użyciem interfejsów API Home.
2. Konfiguruję projekt
Ten diagram przedstawia architekturę aplikacji z interfejsami API Home:
- Kod aplikacji: podstawowy kod, nad którym pracują deweloperzy, aby tworzyć interfejs użytkownika aplikacji i logikę interakcji z pakietem SDK interfejsów API Home.
- Pakiet SDK interfejsów Home: pakiet SDK interfejsów Home dostarczany przez Google współpracuje z usługą interfejsów Home w GMSCore, aby umożliwić sterowanie urządzeniami inteligentnego domu. Deweloperzy tworzą aplikacje współpracujące z interfejsami API Home, pakując je z pakietem SDK interfejsów API Home.
- 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ł Home w usługach Google Play zawiera usługi, które współpracują z interfejsami API Home.
W tym ćwiczeniu z programowania będziemy rozwijać tematy omówione w artykule Tworzenie aplikacji mobilnej z użyciem interfejsów API Home na Androidzie.
Upewnij się, że na koncie masz skonfigurowane i działające co najmniej 2 obsługiwane urządzenia. W tym ćwiczeniu skonfigurujemy automatyzacje (zmiana stanu urządzenia powoduje wykonanie działania na innym urządzeniu), więc do sprawdzenia wyników będziesz potrzebować 2 urządzeń.
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.
Ten warsztat programistyczny korzysta z przykładów z gałęzi codelab-branch-2
aplikacji Sample App.
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 Tworzeniu aplikacji mobilnej za pomocą interfejsów Home API na Androidzie. Ta gałąź kodu bazuje na pierwszej wersji. Tym razem przykłady pokazują, jak tworzyć automatyzacje. Jeśli ukończyłeś(-aś) poprzednie ćwiczenia z programowania i udało Ci się uruchomić wszystkie funkcje, możesz użyć tego samego projektu w Android Studio, aby ukończyć te ćwiczenia z programowania zamiast codelab-branch-2
.
Gdy skompilowany kod źródłowy będzie gotowy do uruchomienia na urządzeniu mobilnym, przejdź do następnej sekcji.
3. Więcej informacji o automatyzacjach
Automatyzacje to zestaw instrukcji „jeśli to, to to”, które mogą automatycznie kontrolować stany urządzenia na podstawie wybranych czynników. Deweloperzy mogą używać automatyzacji do tworzenia zaawansowanych interaktywnych funkcji w swoich interfejsach API.
Automatyzacje składają się z 3 rodzajów komponentów, zwanych nodes: poleceniami inicjującymi, działaniami i warunkami. Te węzły współpracują ze sobą, aby automatyzować działania za pomocą inteligentnych urządzeń domowych. Zazwyczaj są one oceniane w tej kolejności:
- Starter – określa początkowe warunki, które aktywują automatyzację, np. zmianę wartości cechy. Automatyzacja musi mieć Starter.
- Warunek – wszelkie dodatkowe ograniczenia do oceny po uruchomieniu automatyzacji. Aby działania automatyzacji mogły się wykonać, wyrażenie w Warunku musi być prawdziwe.
- Działanie – polecenia lub aktualizacje stanu wykonywane po spełnieniu wszystkich warunków.
Możesz na przykład utworzyć automatyzację, która przyciemnia światła w pokoju, gdy włączysz przełącznik, a telewizor w tym pokoju jest włączony. W tym przykładzie:
- Starter – przełącznik w pokoju jest włączony.
- Warunek – stan włączenia telewizora jest oceniany jako włączony.
- Działanie – światła w tym samym pomieszczeniu co przełącznik są przyciemnione.
Te węzły są oceniane przez Automation Engine w sposób szeregowy lub równoległy.
Przepływ sekwencyjny zawiera węzły, które są wykonywane w kolejności. Zwykle są to polecenia inicjujące, warunki i działania.
Przepływ równoległy może zawierać wiele węzłów działania, które są wykonywane jednocześnie, np. włączanie wielu świateł jednocześnie. Węzły korzystające z przepływu równoległego nie będą się wykonywać, dopóki nie zakończą się 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 interfejsów API Home. 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 Automation Engine za pomocą języka specyficznego dla domeny (DSL) stworzonego specjalnie na potrzeby automatyzacji w Google Home.
Poznawanie języka programowania automatyzacji
Język specyficzny dla danej dziedziny (DSL) to język używany do opisywania zachowania systemu w kodzie. Kompilator generuje klasy danych, które są serializowane do formatu JSON protokołu Buforów i służą do wywoływania usług automatyzacji Google.
DSL szuka tego schematu:
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 w przypadku automatyzacji obowiązują limity zasobów.
Automatyzacje to bardzo przydatne narzędzie do tworzenia automatycznych funkcji w inteligentnym domu. W najprostszym przypadku możesz zaprogramować automatyzację, aby używała określonych urządzeń i właściwości. Bardziej praktyczny przypadek użycia to taki, w którym aplikacja pozwala użytkownikowi konfigurować urządzenia, polecenia i parametry automatyzacji. W następnej sekcji wyjaśniamy, jak utworzyć edytor automatyzacji, który pozwoli użytkownikowi wykonywać te czynności.
4. Tworzenie edytora automatyzacji
W próbnej aplikacji utworzymy edytor automatyzacji, za pomocą którego użytkownicy będą mogli wybierać urządzenia, funkcje (czynności), których chcą używać, oraz sposób uruchamiania automatyzacji za pomocą elementów uruchamiających.
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 przykładowej aplikacji 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ą elementu StarterView
(StarterView.kt
).
W węźle startowym muszą się znaleźć te elementy:
- Urządzenie
- Cecha
- Operacja
- Wartość
Urządzenie i cechę można wybrać spośród 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óra wymaga osobnego podejścia.
Aplikacja definiuje wstępnie 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 Sample App śledzi wartości przypisane do cech:
enum class OnOffValue {
On,
Off,
}
enum class ThermostatValue {
Heat,
Cool,
Off,
}
Aplikacja śledzi 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 renderować wszystkie urządzenia startowe i wdrożyć funkcję wywołania zwrotnego 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 renderować wszystkie cechy urządzenia startowego i zaimplementować funkcję zwrotną 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 renderować wszystkie operacje wybranej cechy i wdrażać funkcję zwrotu wywołania po kliknięciu 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 wybranej cechy i wdrażać funkcję zwrotu wywołania po kliknięciu 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 przechowywać wszystkie zmienne w poziomie startera ViewModel
w poziomie startera automatyzacji roboczej ViewModel
(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)) }
Po uruchomieniu aplikacji i wybraniu nowej automatyzacji i startera powinien wyświetlić się widok podobny do tego:
Przykładowa aplikacja obsługuje tylko startery oparte na cechach urządzenia.
Konfigurowanie działań
Działanie automatyzacji odzwierciedla jej główny cel i to, jak wpływa na zmiany w świecie fizycznym. W aplikacji Sample App działania automatyzacji rejestrujemy za pomocą klasy ActionViewModel
, a widok edytora wyświetlamy za pomocą klasy ActionView
.
Przykładowa aplikacja używa tych elementów interfejsów API Home do definiowania węzłów działań automatyzacji:
- Urządzenie
- Cecha
- Polecenie
- Wartość (opcjonalnie)
Każde polecenie urządzenia używa polecenia, ale niektóre z nich wymagają też wartości parametru, np. MoveToLevel()
i docelowy odsetek.
Urządzenie i cechę można wybrać spośród 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, występuje też zmienna:
val valueLevel: MutableStateFlow<UByte?>
Interfejs API wyświetla zestaw elementów widoku, których użytkownicy mogą używać do wybierania wymaganych pól.
Odkomentuj krok 4.2.1 w pliku ActionView.kt
, aby renderować wszystkie urządzenia akcji, i wprowadź funkcję wywołania zwrotnego 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 renderować wszystkie cechy elementu actionDeviceVM
, i wprowadź funkcję zwrotną kliknięcia w pliku 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 renderować wszystkie dostępne działania funkcji actionTrait
, i wdróż wywołanie zwrotne kliknięcia w funkcji DropdownMenu
, aby ustawić zmienną actionAction
, która 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 renderować dostępne wartości działania atrybutu (polecenia) i zapisywać je w polu actionValueLevel
w zwrotnym wywołaniu 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 przechowywać wszystkie zmienne działania ViewModel
w dziale działania wersji roboczej automatyzacji ViewModel
(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 i działania powinno spowodować wyświetlenie widoku podobnego do tego:
W aplikacji Sample App obsługujemy tylko działania oparte na cechach urządzenia.
Renderowanie wersji roboczej automatyzacji
Gdy DraftViewModel
zostanie ukończone, można je renderować za pomocą 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 już wiesz, jak tworzyć startery i działania, możesz utworzyć wersję roboczą automatyzacji i przesłać ją do Automation API. Interfejs API zawiera funkcję createAutomation()
, która przyjmuje jako argument projekt automatyzacji i zwraca nowy przypadek użycia automatyzacji.
Przygotowanie projektu automatyzacji odbywa się w klasie DraftViewModel
w Sample App. Aby dowiedzieć się więcej o tym, jak tworzymy projekt automatyzacji za pomocą zmiennych startowych i działania w poprzedniej sekcji, zajrzyj do funkcji getDraftAutomation()
.
Odkomentuj krok 4.4.1 w pliku DraftViewModel.kt
, aby utworzyć wyrażenia „select” wymagane do utworzenia wykresu automatyzacji, gdy cecha startowa ma wartość 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 wybrana cecha działania to LevelControl
, a wybrane działanie to 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 do ukończenia automatyzacji jest wdrożenie funkcji getDraftAutomation
w celu utworzenia AutomationDraft.
.
Odkomentuj krok 4.4.3 w pliku HomeAppViewModel.kt
, aby utworzyć automatyzację przez wywołanie interfejsów API Home 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 aplikację i zobacz zmiany na swoim urządzeniu.
Po wybraniu początku i czynności możesz utworzyć automatyzację:
Nadaj automatyzacji unikalną nazwę, a potem kliknij przycisk Utwórz automatyzację. Powinien on wywołać interfejsy API i przywrócić widok listy automatyzacji z automatyzacją:
Kliknij utworzoną właśnie automatyzację i zobacz, jak jest ona zwracana przez interfejsy API.
Pamiętaj, że interfejs API zwraca wartość wskazującą, czy automatyzacja jest prawidłowa i czy jest obecnie aktywna. Można tworzyć automatyzacje, które nie przechodzą weryfikacji podczas analizowania po stronie serwera. Jeśli parsowanie automatyzacji nie przejdzie weryfikacji, isValid
zostanie ustawione na false
, co oznacza, że automatyzacja jest nieprawidłowa i nieaktywna. Jeśli automatyzacja jest nieprawidłowa, sprawdź szczegóły w polu automation.validationIssues
.
Upewnij się, że automatyzacja jest prawidłowa i aktywna, a potem wypróbuj ją.
Testowanie automatyzacji
Automatyzacje można wykonywać na 2 sposoby:
- za pomocą zdarzenia inicjującego, Jeśli warunki są spełnione, uruchamia się działanie określone w automatyzacji.
- za pomocą wywołania interfejsu API do ręcznego wykonania.
Jeśli automatyzacja w postaci szkicu ma w bloku DSL szkicu automatyzacji zdefiniowany element manualStarter()
, mechanizm automatyzacji będzie obsługiwał ręczne uruchamianie tej automatyzacji. Jest on już obecny w przykładach kodu w aplikacji Sample App.
Ponieważ nadal jesteś na ekranie podglądu automatyzacji na urządzeniu mobilnym, kliknij przycisk Ręczne wykonanie. Powinien on wywołać automation.execute()
, który wykona polecenie działania na urządzeniu wybranym podczas konfigurowania automatyzacji.
Po sprawdzeniu polecenia działania za pomocą ręcznego wykonania za pomocą interfejsu API możesz sprawdzić, czy działa ono też za pomocą zdefiniowanego przez Ciebie startera.
Otwórz kartę Urządzenia, wybierz urządzenie akcji i właściwość, a następnie ustaw inną wartość (np. ustaw light2
LevelControl
(jasność) na 50%, jak pokazano na poniższym zrzucie ekranu:
Spróbujemy teraz uruchomić automatyzację za pomocą urządzenia startowego. Wybierz urządzenie startowe wybrane podczas tworzenia automatyzacji. Przełącz wybraną cechę (np. ustaw wartość starter outlet1
na OnOff
):On
Spowoduje to również wykonanie automatyzacji i ustawienie właściwości light2
urządzenia akcji LevelControl
na pierwotną wartość 100%:
Gratulacje! Udało Ci się użyć interfejsów API Home do tworzenia automatyzacji.
Więcej informacji o interfejsie Automation API znajdziesz w artykule Interfejs Automation API na Androida.
5. Poznawanie funkcji
Interfejsy API Home obejmują interfejs Discovery API, którego programiści mogą używać do wysyłania zapytań o to, które cechy obsługujące automatyzację są obsługiwane na danym urządzeniu. Przykładowa aplikacja zawiera przykłady, w których możesz użyć tego interfejsu API, aby dowiedzieć się, jakie polecenia są dostępne.
Odkrywanie poleceń
W tej sekcji opisujemy, jak odkryć obsługiwane CommandCandidates
i jak utworzyć automatyzację na podstawie odkrytych węzłów kandydatów.
W próbnej aplikacji wywołujemy funkcję device.candidates()
, aby uzyskać listę kandydatów, która może zawierać wystąpienia CommandCandidate
, EventCandidate
lub TraitAttributesCandidate
.
Otwórz plik HomeAppViewModel.kt
i usuń komentarz 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 definicji commandMap
w pliku 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 Sample App, omówimy, jak zintegrować to z naszym edytorem.
Więcej informacji o interfejsie Discovery API znajdziesz w artykule Korzystanie z wyszukiwania urządzeń z Androidem.
Integracja edytora
Najczęstszym sposobem korzystania z odkrytych działań jest ich prezentowanie użytkownikowi do wyboru. Tuż przed wybraniem przez użytkownika pól wersji roboczej automatyzacji możemy wyświetlić mu listę odkrytych działań, a w zależności od wybranej przez niego 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 odkryte kandydatury. Odkomentuj krok 5.2.1, aby włączyć funkcję .clickable{}
funkcji CandidateListItem
, która ustawia wartość homeAppVM.selectedDraftVM
na 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 etapie 4.3 w HomeAppView.kt
, gdy ustawisz selectedDraftVM
, zostanie wyrenderowany plik 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, jak pokazano w poprzedniej sekcji. Spowoduje to utworzenie nowej automatyzacji na podstawie polecenia kandydata:
Teraz, gdy już wiesz, jak tworzyć automatyzacje w aplikacji Sample, możesz je integrować ze swoimi aplikacjami.
6. Przykłady ulepszonej automatyzacji
Zanim zakończymy, omówimy jeszcze kilka dodatkowych przykładów automatyzacji DSL. Przykłady te pokazują niektóre z zaawansowanych funkcji, które możesz wykorzystać dzięki interfejsom API.
Godzina jako starter
Oprócz właściwości urządzenia interfejsy API Google Home oferują właściwości oparte na strukturze, takie jak Time
. Możesz utworzyć automatyzację, która ma element startowy oparty na czasie, np.:
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
...
}
}
Asystent – wysyłanie powiadomień jako działania
Właściwość AssistantBroadcast
jest dostępna jako właściwość na poziomie urządzenia w usługach SpeakerDevice
(jeśli głośnik ją obsługuje) lub jako właściwość 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 właściwości DelayFor
i suppressFor
Interfejs Automation API udostępnia też zaawansowane operatory, takie jak delayFor, który służy do opóźniania poleceń, i suppressFor, który może uniemożliwić automatyzacji reagowanie na te same zdarzenia w danym 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 starterze
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 te zaawansowane funkcje automatyzacji, zacznij tworzyć niesamowite aplikacje.
7. Gratulacje!
Gratulacje! Udało Ci się ukończyć drugą część procesu tworzenia aplikacji na Androida przy użyciu interfejsów API Google Home. W tym ćwiczeniu z programowania poznasz interfejsy API automatyzacji i wyszukiwania.
Mamy nadzieję, że tworzenie aplikacji, które umożliwiają kreatywne sterowanie urządzeniami w ekosystemie Google Home, i tworzenie ekscytujących scenariuszy automatyzacji za pomocą interfejsów API Home będzie dla Ciebie przyjemnością.
Dalsze kroki
- Aby dowiedzieć się, jak skutecznie debugować aplikacje i rozwiązywać problemy związane z interfejsami Home API, przeczytaj artykuł Rozwiązywanie problemów.
- Możesz się z nami skontaktować, aby uzyskać rekomendacje lub zgłosić problemy, korzystając z narzędzia do śledzenia problemów w temacie pomocy dotyczącej inteligentnego domu.