Check if a trait supports a command
Support can also be checked for a trait command. Also use the trait-level
supports
function to check if a command is supported for a particular device.
For example, to check for a device's support of the On/Off trait's
toggle
command:
// 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") }
Send a command to a device
Sending a command is similar to reading a state attribute from a trait. To
turn the device on or off, use the
OnOff
trait's Toggle command, which is defined in the Google Home ecosystem data
model as toggle()
. This method changes onOff
to false
if it is true
, or
to true
if it is false
:
// Calling a command on a trait. try { onOffTrait.toggle() } catch (e: HomeException) { // Code for handling the exception }
All trait commands are suspend
functions and only complete when a response
is returned by the API (such as confirming the device state has changed).
Commands might return an exception if an issue is detected with the
execution flow. As a developer, you should use a try-catch
block to properly
handle these exceptions, and surface detailed information to users on cases where
the errors are actionable. Unhandled exceptions will stop the app runtime and
can result in crashes in your app.
Alternatively, use the off()
or on()
commands to explicitly set the state:
onOffTrait.off() onOffTrait.on()
After sending a command to change the state, once it completes you can read the state as described in Read a device state to handle it in your app. Alternatively, use flows as described in Observe state, which is the preferred method.
Send a command with parameters
Some commands may use parameters, like those on the
OnOff
or
LevelControl
traits:
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(), )
Some commands have optional arguments, which come after the required arguments.
For example, the step
command for the FanControl
trait
has two optional arguments:
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 }
Check if a trait supports an attribute
Some devices may support a Matter trait, but not a
specific attribute. For example, a Cloud-to-cloud device that was
mapped to Matter may not support every
Matter attribute. To handle cases like these, use the
trait-level supports
function and the trait's Attribute
enum to check if
the attribute is supported for a particular device.
For example, to check for a device's support of the On/Off trait's
onOff
attribute:
// 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!") }
Some attributes are nullable in the Matter specification or
the Cloud-to-cloud smart home schema. For these
attributes, you can determine whether a null returned by the attribute is
due to the device not reporting that value, or if the attribute's value
actually is null
, by using isNullable
in addition to supports
:
// 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!") }
Update trait attributes
If you want to change the value of a given attribute, and none of the trait's commands does so, the attribute may support having its value explicitly set.
Whether the value of an attribute can be changed depends on two factors:
- Is the attribute writable?
- Can the value of the attribute change as a side effect of sending a trait command?
The reference documentation for traits and their attributes provides this information.
Therefore, the combinations of properties that dictate how an attribute's value might be changed are:
Read-only and not affected by other commands. This means that the attribute's value does not change. For example, the
currentPosition
attribute of theSwitch
trait.Read-only and affected by other commands. This means that the only way the attribute's value can change is as the result of sending a command. For example, the
currentLevel
attribute of theLevelControl
Matter trait is read-only, but its value can be mutated by commands such asmoveToLevel
.Writeable and not affected by other commands. This means that you can directly change the value of the attribute by using the
update
function of the trait, but there are no commands that will affect the value of the attribute. For example, theWrongCodeEntryLimit
attribute of theDoorLock
trait.Writeable and affected by other commands. This means that you can directly change the value of the attribute by using the
update
function of the trait, and the attribute's value can change as the result of sending a command. For example, theoccupiedCoolingSetpoint
attribute of theThermostat
trait can be written to but also updated with thesetpointRaiseLower
command.
Example of using the update function to change an attribute's value
This example shows how to explicitly set the value of the
DoorLockTrait.WrongCodeEntryLimit
attribute.
To set an attribute value, call the trait's update
function and pass it a mutator function that sets the new value.
It's a good practice to first
verify that the trait supports an attribute.
For example:
var 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) } }
Send multiple commands at once
The Batching API allows a client to send multiple Home APIs device commands in a single payload. The commands are batched into a single payload and executed in parallel, similar to how one might construct a Home API automation using the parallel node, such as the Open blinds before sunrise example. However, the Batching API allows for more complex and sophisticated behaviors than the Automation API, such as the ability to dynamically select devices at runtime according to any criteria.
The commands in one batch can target multiple traits across multiple devices, in multiple rooms, in multiple structures.
Sending commands in a batch allows devices to perform actions simultaneously, which isn't really possible when commands are sent sequentially in separate requests. The behavior achieved using batched commands allows the developer to set the state of a group of devices to match a predetermined aggregate state.
Use the Batching API
There are three basic steps involved in invoking commands through the Batching API:
- Invoke the
Home.sendBatchedCommands()
method. - Within the body of the
sendBatchedCommands()
block, specify the commands to be included in the batch. - Check the results of the sent commands to see whether they succeeded or failed.
Invoke the sendBatchedCommands() method
Call the
Home.sendBatchedCommands()
method. Behind the scenes, this method sets up a lambda expression in a special
batch context.
home.sendBatchedCommands() {
Specify batch commands
Within the body of the sendBatchedCommands()
block, populate batchable
commands. Batchable commands are "shadow" versions of existing Device API
commands that can be used in a batch context, and are named with the added
suffix Batchable
. For example, the
LevelControl
trait's
moveToLevel()
command has a counterpart named
moveToLevelBatchable()
.
Example:
val response1 = add(command1)
val response2 = add(command2)
The batch is automatically sent once all commands have been added to the batch context and execution has left the context.
Responses are captured in
DeferredResponse<T>
objects.
The DeferredResponse<T>
instances can be gathered into an object of any type, such as a
Collection
, or a data class that you define. Whatever type of object you
choose to assemble the responses is what is returned by
sendBatchedCommands()
. For example, the batch context can return two
DeferredResponse
instances in a Pair
:
val (response1, response2) = homeClient.sendBatchedComamnds {
val response1 = add(someCommandBatched(...))
val response2 = add(someOtherCommandBatched(...))
Pair(response1, response2)
}
Alternatively, the batch context can return the DeferredResponse
instances in a custom data class:
// Custom data class
data class SpecialResponseHolder(
val response1: DeferredResponse<String>,
val response2: DeferredResponse<Int>,
val other: OtherResponses
)
data class OtherResponses(...)
Check each response
Outside the sendBatchedCommands()
block, check the responses to determine
whether the corresponding command succeeded or failed. This is done by calling
DeferredResponse.getOrThrow()
, which either:
- returns the result of the executed command,
- or, if the batch scope has not completed or the command was
unsuccessful, throws an error.
You should only check the results outside of the sendBatchedCommands()
lambda scope.
Example
Say you wanted to build an app that uses the Batching API to set up a 'good night' scene that configures all the devices in the home for nighttime, when everyone is asleep. This app should turn out the lights and lock the front and back doors.
Here's one way to approach the task:
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()
}