Control devices on iOS

This guide continues from Access devices and device metadata on iOS and introduces additional examples of device control and access.

To use specific device types or traits, such as the Matter OnOffTrait used in many of the examples here, they must be imported:

import GoogleHomeSDK
import GoogleHomeTypes

Check if a trait supports a command

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 onOffTraitTest.supportsToggleCommand {
  print("onOffTrait supports toggle command")
} else {
  print("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 OnOffTrait 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.
do {
  try await onOffTraitTest.toggle()
} catch let _ as HomeError {
  // Code for handling the exception
}

Commands might return an exception if an issue is detected with the execution flow. As a developer, you should use a do-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:

do {
  try await onOffTraitTest.off()
  try await onOffTraitTest.on()
} catch let _ as HomeError {
  // Code for handling the exception
}

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.

Send a command with parameters

Some commands may use parameters, like those on the OnOffTrait or LevelControlTrait:

offWithEffect

// Turn off the light using the DyingLight effect.
do {
  try await onOffTraitTest.offWithEffect(
    effectIdentifier: Matter.OnOffTrait.EffectIdentifierEnum.dyingLight,
    effectVariant: 0
  )
} catch let _ as HomeError {
  // Code for handling the exception
}

moveToLevel

// Change the brightness of the light to 50%
do {
  try await levelControlTraitTest.moveToLevel(
    level: UInt8(127),
    transitionTime: 0,
    optionsMask: Matter.LevelControlTrait.OptionsBitmap(),
    optionsOverride: Matter.LevelControlTrait.OptionsBitmap()
  )
} catch let _ as HomeError {
  // Code for handling the exception
}

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 isSupported property 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.attributes.$onOff.isSupported {
  print("onOffTrait supports onOff state")
} else {
  print("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 nil returned by the attribute is due to the device not reporting that value, or if the attribute's value actually is nil, by using isNullable in addition to isSupported:

// Check if a nullable attribute is set or is not supported.
if let deviceType = await device.types.get(OnOffLightDeviceType.self) {
  if let onOffTrait = deviceType.traits[Matter.OnOffTrait.self] {
    if onOffTrait.attributes.startUpOnOff == nil {
      if onOffTrait.attributes.$startUpOnOff.isSupported {
        print(
          "onOffTrait supports startUpOnOff and it is nil. Check the spec for the contextual meaning."
        )
      } else {
        print("onOffTrait does not support startUpOnOff!")
      }
    } else {
      print(
        "onOffTrait supports startUpOnOff and it is set to \(String(describing: onOffTrait.attributes.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:

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 an update function that sets the new value. It's a good practice to first verify that the trait supports an attribute.

For example:

if doorLockTraitTest.attributes.$wrongCodeEntryLimit.isSupported {
  let _ = try await doorLockTraitTest.update {
    $0.setWrongCodeEntryLimit(3)
  }
}

Control a device group

In addition to fine-grained control over every member of a group by iterating through its member devices, you can control a DeviceGroup as a single entity. DeviceGroup conforms to the HasParts protocol and exposes the DeviceType parts supported by the devices in the group:

  • await group.part(_:) returns the first instance of the specified DeviceType if the group supports it, or nil otherwise.
  • try await group.parts(_:) returns an array of all top-level parts matching the specified DeviceType.

When you invoke a trait command on a device type part of a DeviceGroup, the SDK relays the command to all associated member devices in the group that support that capability, regardless of their underlying connectivity or transport (such as Matter or Cloud-to-cloud).

Group part trait attributes and error handling

Targeting a group part simplifies common cases where you want to send an idempotent command (such as turning lights on or off, or setting a brightness level) across a group. When working with traits and commands on a DeviceGroup part, keep the following behaviors in mind:

  • Dynamic state attributes (such as the onOff attribute of the OnOffTrait or the currentLevel attribute of the LevelControlTrait) are generally not populated on a group part's trait because member devices in a group can be in different states that cannot be combined into a single value. However, supported commands (checked with supports(_:), such as .moveToLevelWithOnOff on a dimmable light) and select capability attributes (such as colorTempPhysicalMinMireds and colorTempPhysicalMaxMireds on ColorControlTrait for color lights) are populated so that your app can configure UI controls appropriately.
  • Not all commands on traits of group members are available on a group part. In particular, commands that return values are not available.
  • A command on a group part does not return individual results for each underlying member device command. To inspect individual command results or call commands that return values, iterate through the group's member devices directly.
  • Commands sent to a part on a DeviceGroup complete once the entire fanout across member devices finishes and all underlying command successes or failures are collected. A command only fails (throws a HomeError) if the command fails for every associated member device or if all target devices are unavailable, surfacing as a single command failure matching the single-command call syntax. If at least one member device executes the command, the group command completes without throwing an error.

Send commands to a group part

To turn on all OnOffLightDeviceType lights in a device group:

if let lightPart = await group.part(OnOffLightDeviceType.self),
   let onOffTrait = lightPart.matterTraits.onOffTrait,
   onOffTrait.supports(.on) {
  try await onOffTrait.on()
}

You can also check multiple related device types (for example, turning off any light device type in the group that supports OnOffTrait):

func onOffTrait(in group: DeviceGroup) async -> Matter.OnOffTrait? {
  if let light = await group.part(OnOffLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  if let light = await group.part(DimmableLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  if let light = await group.part(ColorTemperatureLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  if let light = await group.part(ExtendedColorLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  return nil
}

if let trait = await onOffTrait(in: group) {
  try await trait.off()
}

You can check command support or read capability bounds on a group part before sending a command (for example, checking .moveToLevelWithOnOff on a dimmable light part or reading colorTempPhysicalMinMireds and colorTempPhysicalMaxMireds on a color temperature light part):

if let dimmableLight = await group.part(DimmableLightDeviceType.self),
   let levelTrait = dimmableLight.matterTraits.levelControlTrait,
   levelTrait.supports(.moveToLevelWithOnOff) {
  try await levelTrait.moveToLevelWithOnOff(
    level: 127,
    transitionTime: nil,
    optionsMask: [],
    optionsOverride: []
  )
}

if let colorLight = await group.part(ColorTemperatureLightDeviceType.self),
   let colorTrait = colorLight.matterTraits.colorControlTrait,
   let minMireds = colorTrait.attributes.colorTempPhysicalMinMireds,
   let maxMireds = colorTrait.attributes.colorTempPhysicalMaxMireds {
  let midMireds = UInt16((UInt32(minMireds) + UInt32(maxMireds)) / 2)
  try await colorTrait.moveToColorTemperature(
    colorTemperatureMireds: midMireds,
    transitionTime: 0,
    optionsMask: [],
    optionsOverride: []
  )
}