DSL guide - basic automations

Use the following guide to understand how various Automation DSL nodes may be used to construct an automation.

All automation DSL is placed within a single automation node. The automation node forms the boundary between the outer Kotlin language context and the embedded DSL context.

Sequential flow

The sequential flow is the default type of automation flow.

Sequential DSL example

Here's a very basic Automation DSL template that uses a sequential flow consisting of a starter, a condition, and an action:


import com.google.home.automation.action
import com.google.home.automation.automation
import com.google.home.automation.condition
import com.google.home.automation.sequential
import com.google.home.automation.starter

...

automation {
  sequential {
    starter<_>(...)
    condition {...}
    action {...}
  }
}

This can be refined by adding additional nodes.

Starter

Starter nodes define the initial circumstances that activate an automation. For example, a change in state or value. An automation must have at least one starter, otherwise it will fail validation. In order to add more than one starter to an automation, you must use a select node.

Starter based on trait attribute

When declaring a starter node that's based on a trait attribute, specify:

  • the device
  • the device type to which the trait belongs
  • the trait
starter<_>(thermostat, TemperatureSensorDevice, TemperatureMeasurement)

The device type parameter is required because it lets you specify which device type within a device the automation addresses. For example, a device might be composed of a FanDevice and a HeatingCoolingUnitDevice, both of which contain the OnOff trait. By specifying the device type, there is no ambiguity about which part of the device triggers the automation.

Starter based on event

When declaring a starter node that's based on an event, specify:

  • the device
  • the device type to which the trait belongs
  • the event
starter<_>(doorBell, GoogleDoorbellDevice, DoorbellPressed)

Starter based on a structure and event, with parameters

Some events can have parameters, so these parameters also need to be included in the starter.

For example, this starter uses the Time trait's ScheduledTimeEvent to activate the automation at 7:00am:

val earlyMorning = starter<_>(structure, Time.ScheduledTimeEvent) {
  parameter(Time.ScheduledTimeEvent.clockTime(
    LocalTime.of(7, 0, 0, 0)))
}

Manual starter

A manual starter is a special type of starter that lets the user manually run the automation.

When declaring a manual starter:

When placing a manual starter in a select flow along with another starter, the manual starter overrides the other starter:

select {
  manualStarter()
  starter<_>(thermostat, TemperatureSensorDevice, TemperatureMeasurement)
}

Note that any condition nodes following a manual starter will be evaluated, and could block execution of the automation, depending on the condition expression.

Separating a manual starter from a conditional

One way to structure your automation so that condition nodes don't block an automation that was activated with a manual starter is to put the other starter in a separate sequential flow along with its condition:

automation_graph {
  sequential {
    select {
      sequential {
        starter<_>(...)
        condition {...}
      }
      sequential {
        manualStarter()
      }
    }
    action {...}
  }
}

Reference the value of an attribute

To use the value of an attribute in an expression, use the following syntax.

With a stateReader:

val time = stateReader<_>(structure, Structure, Time)
val currTime = time.currentTime

With a starter:

val starterNode = starter<_>(device1, LaundryWasherDevice, OnOff)
condition() {
  expression = starterNode.onOff equals true
}

Condition nodes and expressions

A condition node represents a decision point that determines whether the automation continues or not. An automation can have multiple condition nodes. If any condition node's expression evaluates to false, execution of the entire automation ends.

Within a condition node, you can combine multiple condition criteria using various operators, as long as the expression evaluates to a single boolean value. If the resulting value is true, the condition is met and the automation continues execution of the next node. If it's false, the automation stops executing at that point.

Expressions are formed similarly to expressions in Kotlin, and may contain primitive values such as numbers, characters, strings, and booleans, as well as Enum values. Grouping sub expressions with parentheses lets you control the order in which they're evaluated.

Here's an example of a condition which combines multiple subexpressions into a single expression:

condition() {
  val expr1 = starterNode.lockState equals DlLockState.Unlocked
  val expr2 = stateReaderNode.lockState equals true

  val expr3 = occupancySensingDevice.occupied notEquals 0
  val expr4 = timeStateReaderNode
    .currentTime
    .between(
      timeStateReaderNode.sunsetTime,
      timeStateReaderNode.sunriseTime)
  expression = (expr1 and expr2) or (expr3 and expr4)
}

You may reference the value of a trait accessed through a starter:

val starterNode = starter<_>(device, OnOff)
condition() { expression = starterNode.onOff equals true }

stateReader

The other way to reference trait attribute values in a condition node is with a stateReader node.

To do this, first capture the trait attribute value in a stateReader node. A stateReader takes the structure and the trait as arguments:

import com.google.home.automation.stateReader
...
val filterMonitoringState = stateReader<_>(structure, ActivatedCarbonFilterMonitoring)

Then reference the stateReader in the condition node:

condition() {
  expression =
    filterMonitoringState.changeIndication
      .equals(ChangeIndicationEnum.Warning)
}

Using comparison and logical operators, multiple stateReaders may be used in a condition node:

val armState = stateReader<_>(doorLock, DoorLockDevice, ArmDisarm )
val doorLockState = stateReader<_>(doorLock, DoorLockDevice, DoorLock)
condition() {
  expression =
    (armState.armState equals true)
    and
    (doorLockState.lockState equals true)
}

Condition duration

In addition to a boolean expression in a condition, you can specify a timeframe during which the expression must be true in order to run the automation. For example, you can define a condition that fires only if a light has been on for ten minutes.

  condition {
    expression(lightStateReader.onOff == true)
    forDuration(Duration.ofMinutes(10))
  }

The duration can range from one to 30 minutes.

Action nodes

The action node is where the work of the automation takes place. In this example, the action invokes the AssistantBroadcast trait's broadcast() command:

action(device, SpeakerDevice) {
  command(AssistantBroadcast.broadcast("Intruder detected!"))
}

Import statements

When developing automations, it isn't always obvious how to import various elements of the Home APIs into your code.

Trait attributes are imported from the trait's Companion object:

import com.google.home.matter.standard.OnOff.Companion.onOff

Data structures that are defined by a trait are imported from the trait class whose name ends in "-Trait":

import com.google.home.matter.standard.MediaPlaybackTrait.PlaybackStateEnum

Trait commands are imported from the trait's Companion object:

import com.google.home.matter.standard.Thermostat.Companion.setTemperatureSetpointHold