Sterowanie urządzeniami z Androidem

Sprawdzanie, czy cecha obsługuje polecenie

Można też sprawdzić, czy polecenie cechy jest obsługiwane. Użyj też funkcji supports na poziomie cechy, aby sprawdzić, czy polecenie jest obsługiwane na danym urządzeniu.

Aby na przykład sprawdzić, czy urządzenie obsługuje polecenie toggle cechy Wł./wył.:

// Check if the OnOff trait supports the toggle command.
if (onOffTrait.supports(OnOff.Command.Toggle)) {
  println("onOffTrait supports toggle command")
} else {
  println("onOffTrait does not support stateful toggle command")
}

Wysyłanie polecenia na urządzenie

Wysyłanie polecenia jest podobne do odczytywania atrybutu stanu z cechy. Aby włączyć lub wyłączyć urządzenie, użyj polecenia Toggle atrybutu OnOff, które w modelu danych ekosystemu Google Home jest zdefiniowane jako toggle(). Ta metoda zmienia onOff na false, jeśli jest to true, lub na true, jeśli jest to false:

// Calling a command on a trait.
try {
  onOffTrait.toggle()
} catch (e: HomeException) {
  // Code for handling the exception
}

Wszystkie polecenia cech to funkcje suspend, które kończą się dopiero po zwróceniu odpowiedzi przez interfejs API (np. potwierdzenia, że stan urządzenia uległ zmianie). Polecenia mogą zwracać wyjątek, jeśli zostanie wykryty problem z przepływem wykonywania. Jako deweloper musisz użyć bloku try-catch, aby prawidłowo obsługiwać te wyjątki i wyświetlać użytkownikom szczegółowe informacje o przypadkach, w których błędy można naprawić. Nieobsługiwane wyjątki zatrzymują działanie aplikacji i mogą powodować jej awarie.

Możesz też użyć poleceń off() lub on(), aby wyraźnie ustawić stan:

onOffTrait.off()
onOffTrait.on()

Po wysłaniu polecenia zmiany stanu i jego wykonaniu możesz odczytać stan zgodnie z opisem w sekcji Odczytywanie stanu urządzenia, aby obsłużyć go w aplikacji. Możesz też użyć przepływów zgodnie z opisem w sekcji Obserwowanie stanu, co jest preferowaną metodą.

Wysyłanie polecenia z parametrami

Niektóre polecenia mogą używać parametrów, takich jak te w atrybutach OnOff lub LevelControl:

offWithEffect

// Turn off the light using the DyingLight effect.
onOffTrait.offWithEffect(
  effectIdentifier = OnOffTrait.EffectIdentifierEnum.DyingLight,
  effectVariant = 0u,
)

moveToLevel

// Change the brightness of the light to 50%
levelControlTrait.moveToLevel(
  level = 127u.toUByte(),
  transitionTime = null,
  optionsMask = LevelControlTrait.OptionsBitmap(),
  optionsOverride = LevelControlTrait.OptionsBitmap(),
)

Niektóre polecenia mają opcjonalne argumenty, które występują po wymaganych argumentach.

Na przykład polecenie step dla cechy FanControl ma 2 opcjonalne argumenty:

val fanControlTraitFlow: Flow<FanControl?> =
  device.type(FanDevice).map { it.standardTraits.fanControl }.distinctUntilChanged()

val fanControl = fanControlTraitFlow.firstOrNull()

// Calling a command with optional parameters not set.
fanControl?.step(direction = FanControlTrait.StepDirectionEnum.Increase)

// Calling a command with optional parameters.
fanControl?.step(direction = FanControlTrait.StepDirectionEnum.Increase) { wrap = true }

Sprawdzanie, czy cecha obsługuje atrybut

Niektóre urządzenia mogą obsługiwać cechę Matter, ale nie konkretny atrybut. Na przykład urządzenie Cloud-to-cloud, które zostało zmapowane na Matter, może nie obsługiwać wszystkich atrybutów Matter. Aby obsłużyć takie przypadki, użyj funkcji supports na poziomie cechy i wyliczenia Attribute cechy, aby sprawdzić, czy atrybut jest obsługiwany na danym urządzeniu.

Aby na przykład sprawdzić, czy urządzenie obsługuje atrybut onOff cechy On/Off:

// Check if the OnOff trait supports the onOff attribute.
if (onOffTrait.supports(OnOff.Attribute.onOff)) {
  println("onOffTrait supports onOff state")
} else {
  println("onOffTrait is for a command only device!")
}

Niektóre atrybuty mogą mieć wartość null w specyfikacji Matter lub schemacie Cloud-to-cloud smart home. W przypadku tych atrybutów możesz określić, czy zwracana przez atrybut wartość null wynika z tego, że urządzenie nie zgłasza tej wartości, czy też wartość atrybutu to rzeczywiście null. W tym celu oprócz supports użyj isNullable:

// Check if a nullable attribute is set or is not supported.
if (onOffTrait.supports(OnOff.Attribute.startUpOnOff)) {
  // The device supports startupOnOff, it is safe to expect this value in the trait.
  if (OnOff.Attribute.startUpOnOff.isNullable && onOffTrait.startUpOnOff == null) {
    // This value is nullable and set to null. Check the specification as to
    // what null in this case means
    println("onOffTrait supports startUpOnOff and it is null")
  } else {
    // This value is nullable and set to a value.
    println("onOffTrait supports startUpOnOff and it is set to ${onOffTrait.startUpOnOff}")
  }
} else {
  println("onOffTrait does not support startUpOnOff!")
}

Aktualizowanie atrybutów cech

Jeśli chcesz zmienić wartość danego atrybutu, a żadne z poleceń cechy tego nie robi, atrybut może obsługiwać jawne ustawienie wartości.

To, czy można zmienić wartość atrybutu, zależy od 2 czynników:

  • Czy atrybut jest zapisywalny?
  • Czy wartość atrybutu może się zmienić jako efekt uboczny wysłania polecenia cechy?

Te informacje znajdziesz w dokumentacji referencyjnej cech i ich atrybutów.

Dlatego kombinacje właściwości, które określają, jak można zmienić wartość atrybutu, to:

  • Tylko do odczytu i nie wpływają na nie inne polecenia. Oznacza to, że wartość atrybutu nie ulega zmianie. Na przykład atrybut currentPosition cechy Switch.

  • Tylko do odczytu i pod wpływem innych poleceń. Oznacza to, że jedynym sposobem na zmianę wartości atrybutu jest wysłanie polecenia. Na przykład atrybut currentLevel cechy LevelControl Matter jest tylko do odczytu, ale jego wartość można zmienić za pomocą poleceń takich jak moveToLevel.

  • Można je zapisywać i nie mają na nie wpływu inne polecenia. Oznacza to, że możesz bezpośrednio zmienić wartość atrybutu za pomocą funkcji update cechy, ale nie ma poleceń, które wpływają na wartość atrybutu. Na przykład atrybut WrongCodeEntryLimit cechy DoorLock.

  • Można je zapisywać i wpływają na nie inne polecenia. Oznacza to, że możesz bezpośrednio zmienić wartość atrybutu, używając funkcji update cechy, a wartość atrybutu może się zmienić w wyniku wysłania polecenia. Na przykład atrybut speedSetting elementu FanControlTrait można zapisać bezpośrednio, ale można go też zmienić za pomocą polecenia step.

Przykład użycia funkcji aktualizacji do zmiany wartości atrybutu

Ten przykład pokazuje, jak jawnie ustawić wartość atrybutu DoorLockTrait.WrongCodeEntryLimit.

Aby ustawić wartość atrybutu, wywołaj funkcję update cechy i przekaż jej funkcję mutatora, która ustawia nową wartość. Dobrą praktyką jest najpierw sprawdzić, czy cecha obsługuje dany atrybut.

Na przykład:

    val doorLockDevice = home.devices().list().first { device -> device.has(DoorLock) }

    val traitFlow: Flow<DoorLock?> =
      doorLockDevice.type(DoorLockDevice).map { it.standardTraits.doorLock }.distinctUntilChanged()

    val doorLockTrait: DoorLock = traitFlow.first()!!

    if (doorLockTrait.supports(DoorLock.Attribute.wrongCodeEntryLimit)) {
      val unused = doorLockTrait.update { setWrongCodeEntryLimit(3u) }
    }

Wysyłanie kilku poleceń jednocześnie

Interfejs Batching API umożliwia klientowi wysyłanie wielu poleceń do urządzeń Home API w jednym ładunku. Polecenia są grupowane w jednym ładunku i wykonywane równolegle, podobnie jak w przypadku tworzenia automatyzacji interfejsu Home API za pomocą węzła równoległego, np. Otwórz rolety przed wschodem słońca. Interfejs Batching API umożliwia jednak bardziej złożone i zaawansowane działania niż interfejs Automation API, takie jak dynamiczne wybieranie urządzeń w czasie działania według dowolnych kryteriów.

Polecenia w jednej grupie mogą dotyczyć wielu cech na wielu urządzeniach, w wielu pomieszczeniach i w wielu budynkach.

Wysyłanie poleceń w pakiecie umożliwia urządzeniom wykonywanie działań jednocześnie, co nie jest możliwe, gdy polecenia są wysyłane sekwencyjnie w oddzielnych żądaniach. Dzięki poleceniom grupowym deweloper może ustawić stan grupy urządzeń tak, aby odpowiadał określonemu wcześniej stanowi zbiorczemu.

Korzystanie z interfejsu Batching API

Wywoływanie poleceń za pomocą interfejsu Batching API obejmuje 3 podstawowe kroki:

  1. Wywołaj metodę Home.sendBatchedCommands().
  2. W treści bloku sendBatchedCommands() określ polecenia, które mają być uwzględnione w grupie.
  3. Sprawdź wyniki wysłanych poleceń, aby zobaczyć, czy się powiodły.

Wysyłanie poleceń zbiorczych

Wywołaj metodę Home.sendBatchedCommands(). Za kulisami ta metoda konfiguruje wyrażenie lambda w specjalnym kontekście wsadowym.

home.sendBatchedCommands() {

Określanie poleceń zbiorczych

W treści bloku sendBatchedCommands() wpisz polecenia, które można wykonywać zbiorczo. Polecenia, które można grupować, to „cieniowe” wersje istniejących poleceń interfejsu Device API, których można używać w kontekście grupowym. Są one nazwane z dodanym sufiksem Batchable. Na przykład polecenie moveToLevel() cechy LevelControl ma odpowiednik o nazwie moveToLevelBatchable().

Przykład:

  val response1 = add(command1)

  val response2 = add(command2)

Zostanie on wysłany automatycznie, gdy wszystkie polecenia zostaną dodane do kontekstu wsadowego, a wykonywanie opuści kontekst.

Odpowiedzi są rejestrowane w obiektach DeferredResponse<T>.

Instancje DeferredResponse<T> można zebrać w obiekt dowolnego typu, np. Collection, lub w zdefiniowaną przez Ciebie klasę danych. Niezależnie od tego, jaki typ obiektu wybierzesz do zebrania odpowiedzi, zostanie on zwrócony przez funkcję sendBatchedCommands(). Na przykład kontekst wsadowy może zwracać 2 instancje DeferredResponse w obiekcie Pair:

  val (response1, response2) = homeClient.sendBatchedComamnds {
    val response1 = add(someCommandBatched(...))
    val response2 = add(someOtherCommandBatched(...))
    Pair(response1, response2)
  }

Możesz też zwrócić instancje DeferredResponse w niestandardowej klasie danych:

  // Custom data class
  data class SpecialResponseHolder(
    val response1: DeferredResponse<String>,
    val response2: DeferredResponse<Int>,
    val other: OtherResponses
  )
  data class OtherResponses(...)

Sprawdzanie każdej odpowiedzi

Poza blokiem sendBatchedCommands() sprawdź odpowiedzi, aby określić, czy odpowiednie polecenie zostało wykonane, czy nie. W tym celu wywołaj funkcję DeferredResponse.getOrThrow(), która: - zwraca wynik wykonanego polecenia, - lub, jeśli zakres wsadowy nie został ukończony lub polecenie nie powiodło się, zgłasza błąd.

Wyniki należy sprawdzać poza zakresem funkcji lambda sendBatchedCommands().

Przykład

Załóżmy, że chcesz utworzyć aplikację, która używa interfejsu Batching API do skonfigurowania sceny „dobranoc”, która konfiguruje wszystkie urządzenia w domu na noc, gdy wszyscy śpią. Ta aplikacja powinna wyłączyć światła i zamknąć drzwi wejściowe i tylne.

Oto jeden ze sposobów wykonania tego zadania:

val lightDevices: List<OnOffLightDevice>
val doorlockDevices: List<DoorLockDevice>

// Send all the commands
val responses: List<DeferredResponse<Unit>> = home.sendBatchedCommands {
  // For each light device, send a Batchable command to turn it on
  val lightResponses: List<DeferredResponse<Unit>> = lightDevices.map { lightDevice ->
    add(lightDevice.standardTraits.onOff.onBatchable())
  }

  // For each doorlock device, send a Batchable command to lock it
  val doorLockResponse: List<DeferredResponse<Unit>> = doorlockDevices.map { doorlockDevice ->
    add(doorlockDevice.standardTraits.doorLock.lockDoorBatchable())
  }

  lightResponses + doorLockResponses
}

// Check that all responses were successful
for (response in responses) {
  response.getOrThrow()
}

Sterowanie grupą urządzeń

Oprócz precyzyjnej kontroli nad każdym członkiem grupy przez iterowanie po urządzeniach członków możesz sterować DeviceGroup jako pojedynczą encją. DeviceGroup implementuje interfejs PartsProvider, który udostępnia części DeviceType obsługiwane przez urządzenia w grupie:

  • group.parts() zwraca Flow<Set<DeviceType>> wszystkich typów urządzeń obecnych w grupie.
  • group.part() i group.partOrNull() zwracają proces dla konkretnego typu urządzenia, takiego jak OnOffLightDevice lub DimmableLightDevice.

Gdy wywołasz polecenie cechy na typie urządzenia, który jest częścią DeviceGroup, pakiet SDK przekaże polecenie do wszystkich powiązanych urządzeń w grupie, które obsługują tę funkcję, niezależnie od ich połączenia lub transportu (np. Matter lub Cloud-to-cloud).

Atrybuty cech części grupy i obsługa błędów

Kierowanie na część grupy upraszcza typowe przypadki, w których chcesz wysłać idempotentne polecenie (np. włączenie lub wyłączenie światła albo ustawienie poziomu jasności) do grupy. Podczas pracy z atrybutami i poleceniami w części DeviceGroup pamiętaj o tych kwestiach:

  • Atrybuty stanu dynamicznego (takie jak atrybut onOff cechy OnOff lub atrybut currentLevel cechy LevelControl) zwykle nie są wypełniane w cechach części grupy, ponieważ urządzenia należące do grupy mogą być w różnych stanach, których nie można połączyć w jedną wartość. Obsługiwane polecenia (sprawdzane za pomocą supports(), np. LevelControl.Command.MoveToLevelWithOnOff w przypadku oświetlenia z możliwością ściemniania) i wybrane atrybuty funkcji (np. colorTempPhysicalMinMireds i colorTempPhysicalMaxMireds w przypadku ColorControl dla oświetlenia kolorowego) są jednak wypełniane, aby aplikacja mogła odpowiednio skonfigurować elementy sterujące interfejsu.
  • Nie wszystkie polecenia dotyczące cech członków grupy są dostępne w części grupy. W szczególności niedostępne są polecenia, które zwracają wartości.
  • Polecenie dotyczące części grupy nie zwraca poszczególnych wyników dla każdego polecenia urządzenia podrzędnego. Aby sprawdzić wyniki poszczególnych poleceń lub wywołać polecenia, które zwracają wartości, wykonaj iterację bezpośrednio na urządzeniach należących do grupy.
  • Polecenia wysyłane do części na urządzeniu DeviceGroup są wykonywane po zakończeniu całego procesu fanout na urządzeniach członków i zebraniu wszystkich informacji o powodzeniu lub niepowodzeniu poleceń. Polecenie kończy się niepowodzeniem (zwraca błąd HomeException) tylko wtedy, gdy nie powiedzie się w przypadku wszystkich powiązanych urządzeń członków lub gdy wszystkie urządzenia docelowe są niedostępne. Jest to sygnalizowane jako niepowodzenie pojedynczego polecenia, które odpowiada składni wywołania pojedynczego polecenia. Jeśli co najmniej jedno urządzenie należące do grupy wykona polecenie, polecenie grupowe zostanie wykonane bez zgłaszania wyjątku.

Wysyłanie poleceń do części grupy

Aby włączyć wszystkie światła OnOffLightDevice w grupie urządzeń:

val lightPart = group.partOrNull(OnOffLightDevice).firstOrNull()
val onOffTrait = lightPart?.trait(OnOff)

if (onOffTrait?.supports(OnOff.Command.On) == true) {
  onOffTrait.on()
}

Możesz też sprawdzić wszystkie typy urządzeń w grupie, używając group.parts(), aby kierować cechę na wiele powiązanych typów urządzeń (np. wyłączyć dowolny typ urządzenia oświetleniowego w grupie, który obsługuje OnOff):

val lightTypes = listOf(
  OnOffLightDevice,
  DimmableLightDevice,
  ColorTemperatureLightDevice,
  ExtendedColorLightDevice,
)

val parts = group.parts().first()
val onOffTrait = lightTypes.firstNotNullOfOrNull { type ->
  parts.firstOrNull { it.factory == type }
}?.trait(OnOff)

onOffTrait?.off()

Przed wysłaniem polecenia możesz sprawdzić obsługę poleceń lub odczytać granice możliwości części grupy (np. sprawdzić MoveToLevelWithOnOff w przypadku części oświetlenia z możliwością ściemniania lub odczytać colorTempPhysicalMinMireds i colorTempPhysicalMaxMireds w przypadku części oświetlenia z temperaturą kolorów):

val dimmableLight = group.partOrNull(DimmableLightDevice).firstOrNull()
val levelControl = dimmableLight?.trait(LevelControl)

if (
  levelControl?.supports(
    LevelControl.Command.MoveToLevelWithOnOff
  ) == true
) {
  levelControl.moveToLevelWithOnOff(
    level = 127u,
    transitionTime = null,
    optionsMask = LevelControlTrait.OptionsBitmap(),
    optionsOverride = LevelControlTrait.OptionsBitmap(),
  )
}

val colorLight = group.partOrNull(ColorTemperatureLightDevice).firstOrNull()
val colorControl = colorLight?.trait(ColorControl)
val minMireds = colorControl?.colorTempPhysicalMinMireds
val maxMireds = colorControl?.colorTempPhysicalMaxMireds

if (colorControl != null && minMireds != null && maxMireds != null) {
  val midMireds = ((minMireds.toInt() + maxMireds.toInt()) / 2).toUShort()
  colorControl.moveToColorTemperature(
    colorTemperatureMireds = midMireds,
    transitionTime = 0u,
    optionsMask = ColorControlTrait.OptionsBitmap(),
    optionsOverride = ColorControlTrait.OptionsBitmap(),
  )
}