Build an automation on iOS

The Automation APIs may be accessed through the Home APIs, but since their entry point is through a structure, permission must first be granted on the structure before they can be used.

Once permissions are granted for a structure, import the GoogleHomeSDK and GoogleHomeTypes modules into your app:

import GoogleHomeSDK
import GoogleHomeTypes

GoogleHomeSDK contains APIs for Commissioning, Device & Structure, and Automations. GoogleHomeTypes contains APIs for all traits and device types.

A structure conforms to the AutomationManager protocol with the following automation-specific methods:

API Description
listAutomations() List all automations that belong to the structure. Only automations you have created through the Home APIs are returned.
createAutomation(_:) Create an automation instance for a structure.
deleteAutomation(id:) Delete an automation instance by its ID.
deleteAutomation(_:) Delete an automation object.

Create an automation

After creating an instance of Home and receiving permissions from the user, get the structure and device(s):

let structure = try await self.home.structures().list().first()
let homeDevices = try await self.home.devices().list()

Then define the logic of your automation using Automation DSL for iOS. In the Home APIs, an automation is represented by the Automation interface. This interface contains a set of properties:

  • Metadata, such as name and description.
  • Flags that indicate, for example, whether or not the automation can be executed.
  • A list of nodes that contain the logic of the automation, called the automation graph, represented by the automationGraph property.

automationGraph, by default, is of the type SequentialFlow, which is a class that contains a list of nodes that execute in sequential order. Each node represents an element of the automation, such as a starter, condition, or action.

Assign the automation a name and description.

Creation of an automation defaults the isActive flag to true, therefore it's not necessary to explicitly set this flag unless you initially want the automation to be disabled. In that scenario, set the flag to false during creation.

The DraftAutomation typealias interface is used for building and creating automations, and the Automation interface is used for retrieval. For example, here's the Automation DSL for an automation that turns a device on when another device is turned on:

typealias OnOffTrait = Matter.OnOffTrait

let draftAutomation = automation(
    name: "MyFirstAutomation",
    description: "Turn on a device when another device is turned on."
  ) {
    let onOffStarter = starter(light1, OnOffLightDeviceType.self, OnOffTrait.self)
    onOffStarter
    condition {
      onOffStarter.onOff.equals(true)
    }
    action(light2, OnOffLightDeviceType.self) {
      OnOffTrait.on()
    }
  }

Once the automation is defined, pass it to the createAutomation(_:) method to create the Automation instance:

self.myAutomation = try await structure.createAutomation(draftAutomation)

From here, you can use all the other automation methods on the automation, such as execute(), stop(), and update(_:).

Validation errors

If automation creation does not pass validation, a warning or error message provides information about the issue. For more information, refer to the ValidationIssueType reference.

Code example

Here we present some example code that could be used to implement parts of the hypothetical automations described on the Design an automation on iOS page.

Simple automation

An automation that raises the blinds at 8:00am might be implemented like this:

private func createAutomationThatRaisesBlinds(structure: Structure) async throws -> String {
  // Get all candidates in the structure.
  let allCandidates = try await structure.candidates().list()

  // Determine whether a scheduled automation can be constructed.
  guard
    allCandidates
      .compactMap({ $0.node as? EventCandidate })
      .contains(where: {
        $0.event is Google.TimeTrait.ScheduledEvent.Type
          && $0.unsupportedReasons.isEmpty
      })
  else {
    // Cannot create automation. Set up your address on the structure, then try again.
    return ""
  }

  let blinds =
    allCandidates
    .compactMap({ $0.node as? CommandCandidate })
    .filter {
      $0.command is Matter.WindowCoveringTrait.UpOrOpenCommand.Type
        && $0.unsupportedReasons.isEmpty
    }.compactMap { $0.homeObject as? HomeDevice }.filter {
      $0.types.contains(WindowCoveringDeviceType.self)
    }

  guard blinds.count > 0 else {
    // You don't have any WindowCoveringDevices. Try again after adding some blinds to your
    // structure.
    return ""
  }

  let draftAutomation = automation(
    name: "Day time open blinds",
    description: "Open all blinds at 8AM everyday"
  ) {
    starter(structure, Google.TimeTrait.ScheduledEvent.self) {
      Google.TimeTrait.ScheduledEvent.clockTime(.init(hours: 8, minutes: 0))
    }
    // ...open all blinds
    parallel {
      for blind in blinds {
        action(blind, WindowCoveringDeviceType.self) {
          Matter.WindowCoveringTrait.upOrOpen()
        }
      }
    }
  }

  let automation = try await structure.createAutomation(draftAutomation)
  return automation.id
}

Complex automation

An automation that triggers blinking lights when motion is detected might be implemented like this:

private func createAutomationThatTriggersBlinkingLightsWhenMotionIsDetected(structure: Structure)
  async throws -> String
{
  // Get all candidates in the structure.
  let allCandidates = try await structure.candidates().list()

  // Get the lights present in the structure.
  let availableLights =
    allCandidates
    .compactMap({ $0.node as? CommandCandidate })
    .compactMap({ $0.homeObject as? HomeDevice })
    .filter {
      $0.types.contains(OnOffLightDeviceType.self)
        || $0.types.contains(DimmableLightDeviceType.self)
        || $0.types.contains(ColorTemperatureLightDeviceType.self)
        || $0.types.contains(ExtendedColorLightDeviceType.self)
    }

  let selectedLights = // user selects one or more lights from availableLights

  let draftAutomation = automation {
    // If the presence state changes...
    let areaPresenceStateStarter = starter(structure, Google.AreaPresenceStateTrait.self)
    areaPresenceStateStarter

    // ...and if the area is occupied...
    condition { areaPresenceStateStarter.presenceState.equals(.presenceStateOccupied) }

    // "blink" the light(s)
    parallel {
      for light in selectedLights {
        action(light, OnOffLightDeviceType.self) { Matter.OnOffTrait.toggle() }
        delay(for: .seconds(1))
        action(light, OnOffLightDeviceType.self) { Matter.OnOffTrait.toggle() }
        delay(for: .seconds(1))
        action(light, OnOffLightDeviceType.self) { Matter.OnOffTrait.toggle() }
        delay(for: .seconds(1))
        action(light, OnOffLightDeviceType.self) { Matter.OnOffTrait.toggle() }
      }
    }
  }

  let automation = try await structure.createAutomation(draftAutomation)
  return automation.id
}

Generic automation

The following code shows how one could gather data about the user's home using the Structure API for iOS, the Device API for iOS, and the Discovery API for iOS to implement an app that guides the user through creating their own generic automation (as described in Design an automation: Generic automation).

// Get a snapshot of the structure.
guard
  let structure = try await self.home.structures().list().first(where: { $0.id == structureID })
else {
  return
}

// Dictionary of devices where key is the device ID.
let devicesByID: [String: HomeDevice] = try await self.home.devices().list().reduce(into: [:]) {
  $0[$1.id] = $1
}

// Dictionary of rooms where key is the room name.
let roomsByName: [String: Room] = try await self.home.rooms().list().reduce(into: [:]) {
  $0[$1.name] = $1
}

// Dictionary of action candidates where key is associated trait ID.
var actionsByTrait: [String: [any ActionCandidate]] = [:]

// Dictionary of action candidates where key is device type ID
var actionsByDeviceType: [String: [any ActionCandidate]] = [:]

// Dictionary of action candidates where key is the entity ID.
var actionsByEntity: [String: [any ActionCandidate]] = [:]

// Dictionary of starter candidates where key is the trait ID.
var startersByTrait: [String: [any StarterCandidate]] = [:]

// Dictionary of starter candidates where key is the device type ID.
var startersByDeviceType: [String: [any StarterCandidate]] = [:]

// Dictionary of starter candidates where key is the entity ID.
var startersByEntity: [String: [any StarterCandidate]] = [:]

// Dictionary of state reader candidates where key is the trait ID.
var stateReadersByTrait: [String: [any StateReaderCandidate]] = [:]

// Dictionary of state reader candidates where key is the device type ID.
var stateReadersByDeviceType: [String: [any StateReaderCandidate]] = [:]

// Dictionary of state reader candidates where key is the entity ID.
var stateReadersByEntity: [String: [any StateReaderCandidate]] = [:]

let allCandidates = try await structure.candidates().list()

for candidate in allCandidates {
  if let actionCandidate = candidate.node as? any ActionCandidate {
    actionsByTrait[actionCandidate.trait.identifier, default: []].append(actionCandidate)
    for deviceType in actionCandidate.deviceTypes {
      actionsByDeviceType[deviceType.identifier, default: []].append(actionCandidate)
    }
    actionsByEntity[actionCandidate.homeObject.id, default: []].append(actionCandidate)
  }

  if let starterCandidate = candidate.node as? any StarterCandidate {
    startersByTrait[starterCandidate.trait.identifier, default: []].append(starterCandidate)
    for deviceType in starterCandidate.deviceTypes {
      startersByDeviceType[deviceType.identifier, default: []].append(starterCandidate)
    }
    startersByEntity[starterCandidate.homeObject.id, default: []].append(starterCandidate)
  }

  if let stateReaderCandidate = candidate.node as? any StateReaderCandidate {
    stateReadersByTrait[stateReaderCandidate.trait.identifier, default: []].append(
      stateReaderCandidate)
        for deviceType in stateReaderCandidate.deviceTypes {
      stateReadersByDeviceType[deviceType.identifier, default: []].append(stateReaderCandidate)
    }
    stateReadersByEntity[stateReaderCandidate.homeObject.id, default: []].append(
      stateReaderCandidate)
    }
  }
}

Execute an automation

Run a created automation using the execute() method:

try await self.myAutomation.execute()

If the automation has a ManualStarter, execute() starts the automation from that point, ignoring all nodes that precede the manual starter. If the automation doesn't have a manual starter, execution starts from the node following the first starter node.

If the execute() operation fails, a HomeError may be thrown.

Stop an automation

Stop a running automation using the stop() method:

try await self.myAutomation.stop()

If the stop() operation fails, a HomeError may be thrown.

Get a list of automations for a structure

Automations are defined at the structure level. Call listAutomations() on the structure to access an array of automations:

let automations = try await structure.listAutomations()

Get an automation by ID

To get an automation by automation ID, call the listAutomations() method and match on ID:

guard let structure = try await home.structures().list().first() else {
  return nil
}
return try await structure.listAutomations().first { $0.id == "automation-id" }

Response:

// Here's how the automation looks like in the get response.

Automation(id: "c4b0f847...",
  structure: structure@test-structure,
  name: "automation-name",
  description: "automation-description",
  isActive: true,
  automationGraph: SequentialFlow(nodes: [
    Starter(
      entity: "device@test-device",
      deviceType: "home.matter.0000.types",
      trait: "OnOff@6789..."),
    Action(
      entity: "device@test-device",
      deviceType: "home.matter.0000.types.0101",
      trait: "OnOff@8765...",
      command: "on")
  ]))

Get an automation by name

To get an automation by name, get the structure's automations and filter on the automation name:

guard let structure = try await home.structures().list().first() else {
  return nil
}
return try await structure.listAutomations().first { $0.name == "automation-name" }

Update an automation

To update an automation's metadata, call its update(_:) method, passing it a lambda expression that sets the metadata:

let myAutomation = try await structure.listAutomations().first {
  $0.id == "automation-id"
}
try await myAutomation.update {
  $0.name = "new-automation-name"
  $0.description = "new-automation-description"
}

The update(_:) method is strictly for updating automation metadata, not for manipulating the automation graph. Directly modifying the automation graph is neither practical, recommended, nor supported, because of the interdependencies between the nodes. If you want to change the logic of an automation, regenerate the entire automation graph.

Delete an automation

To delete an automation, use the deleteAutomation() method. An automation must be deleted directly:

let myAutomation = try await structure.listAutomations().first {
  $0.id == "automation-id"
}
try await structure.deleteAutomation(myAutomation)

An automation can also be deleted by ID:

try await structure.deleteAutomation(id: myAutomation.id)

If the deletion fails, a HomeError may be thrown.

Impact of device deletion on automations

If a user deletes a device that is used in an automation, the deleted device can't trigger any starters, and the automation won't be able to read attributes from it, or issue commands to it. For example, if a user deletes an OccupancySensorDeviceType from their home, and an automation has a starter that depends on the OccupancySensorDeviceType, that starter can no longer activate the automation.