Skip to main content

Conditions

Conditions are optional checks that must all pass (AND logic) for a rule's actions to execute. The rule can have zero conditions — it fires on every matching trigger event.

Conditions are evaluated in order. The first failure short-circuits — remaining conditions are not evaluated.

Condition reference

DeviceState

Checks the current value of a device attribute in the database.

[[conditions]]
type = "device_state"
device = "entryway.front_door"
attribute = "open"
op = "Eq"
value = false # door must be closed
FieldDescription
devicePreferred device reference: canonical name, unique display name, or raw device ID
device_idBackward-compatible alias for device
attributeAttribute name
opComparison operator
valueExpected value

Operators (op):

OperatorMeaning
EqEqual to
NeNot equal to
GtGreater than
GteGreater than or equal to
LtLess than
LteLess than or equal to

Examples:

# Light is off
[[conditions]]
type = "device_state"
device = "living_room.main_light"
attribute = "on"
op = "Eq"
value = false

# Temperature above 80°F
[[conditions]]
type = "device_state"
device = "hallway.thermostat"
attribute = "temperature"
op = "Gt"
value = 80

# Motion was detected (any truthy state)
[[conditions]]
type = "device_state"
device = "hallway.motion"
attribute = "motion"
op = "Eq"
value = true

# Battery level below 20%
[[conditions]]
type = "device_state"
device = "entryway.door_sensor"
attribute = "battery"
op = "Lt"
value = 20

TimeWindow

Checks whether the current wall-clock time falls within a window. between is accepted as an alias for time_window.

[[conditions]]
type = "time_window"
start = "08:00"
end = "22:00"

# Alias form
[[conditions]]
type = "between"
start = "08:00"
end = "22:00"

Handles midnight wrap: start = "22:00", end = "06:00" correctly covers 10 PM to 6 AM.


TimeElapsed

Checks how long an attribute has held its current value. Uses an in-memory per-attribute timestamp cache — zero database I/O.

[[conditions]]
type = "time_elapsed"
device = "garage.main_door"
attribute = "open"
duration_secs = 600 # 10 minutes

Passes if the attribute has been in its current value for at least duration_secs seconds.

Common pattern — alert if door has been open for 10 minutes:

[trigger]
type = "cron"
expression = "0 * * * * *" # every minute

[[conditions]]
type = "device_state"
device_id = "yolink_garage_door"
attribute = "open"
op = "Eq"
value = true

[[conditions]]
type = "time_elapsed"
device_id = "yolink_garage_door"
attribute = "open"
duration_secs = 600

[[actions]]
type = "notify"
channel = "telegram"
message = "Garage door has been open for 10+ minutes!"

Startup behavior: At startup, the timestamp cache is pre-populated from device.last_seen as a conservative baseline. TimeElapsed may fire sooner than expected on the first evaluation after restart for attributes that have been in their current state for a long time.


ScriptExpression

Evaluates a Rhai script expression that must return true or false.

[[conditions]]
type = "script_expression"
script = 'device_state("thermostat")["temperature"] > 75 && hour() < 22'

Available Rhai functions:

FunctionReturnsDescription
device_state("device_id")mapCurrent attributes of a device
hour()intCurrent hour (0-23, local time)
minute()intCurrent minute (0-59)
weekday()intDay of week (0=Sun, 1=Mon, …, 6=Sat)
is_weekday()boolTrue if Mon-Fri
is_weekend()boolTrue if Sat-Sun

Examples:

# Complex multi-device condition
[[conditions]]
type = "script_expression"
script = '''
let garage = device_state("yolink_garage_door");
let motion = device_state("motion_garage");
garage["open"] == true && motion["motion"] == false
'''

# Time-based logic not expressible as TimeWindow
[[conditions]]
type = "script_expression"
script = 'hour() >= 22 || hour() < 6' # after 10 PM or before 6 AM

Not

Inverts the result of any wrapped condition.

[[conditions]]
type = "not"

[conditions.condition]
type = "device_state"
device_id = "virtual_switch_away_mode"
attribute = "on"
op = "Eq"
value = true

This reads: "away mode is NOT active."

Nesting: Not can wrap any condition type, including ScriptExpression, TimeWindow, or another Not (double-negation, unusual but valid).


ModeIs

Checks whether a named mode is currently on or off.

[[conditions]]
type = "mode_is"
mode_id = "mode_night"
on = true # "is mode_night active?"
FieldRequiredDescription
mode_idyesMode device id (e.g. "mode_night").
onyestrue to require the mode is on, false to require it's off.

Equivalent to a DeviceState check on the mode's virtual device, but more readable.


PrivateBooleanIs

Checks a rule-local boolean flag set by SetPrivateBoolean action.

[[conditions]]
type = "private_boolean_is"
name = "already_notified"
value = false

Used with SetPrivateBoolean to prevent duplicate notifications:

# Only notify once; set the flag to prevent repeated notifications
[[conditions]]
type = "private_boolean_is"
name = "already_notified"
value = false

[[actions]]
type = "notify"
channel = "telegram"
message = "Alert!"

[[actions]]
type = "set_private_boolean"
name = "already_notified"
value = true

HubVariable

Checks a hub variable's value. Hub variables are shared across all rules and persist for the lifetime of the running process (set via SetHubVariable action or REST).

[[conditions]]
type = "hub_variable"
name = "alarm_armed"
op = "Eq"
value = true
FieldRequiredDescription
nameyesHub variable name.
opyesA CompareOp value: Eq, Ne, Gt, Gte, Lt, Lte, Contains, In.
valueyesComparison value (any JSON type).

DeviceLastChange

Passes when the most recent change to a device matches the supplied provenance filters. Useful for "was this triggered by a person, by a rule, or by a plugin?" patterns.

[[conditions]]
type = "device_last_change"
device = "living_room.lamp"
kind = "physical" # optional — homecore | physical | external | unknown
# source = "plugin.lutron" # optional — exact match on the source label
# actor_id = "..." # optional — exact match on the actor id
# actor_name = "..." # optional — exact match on the actor name
FieldRequiredDescription
device_id (alias device)yesDevice whose last change to inspect.
kindnoFilter on change kind.
sourcenoExact match on the source label (plugin id, rule id, etc.).
actor_id / actor_namenoExact match on actor metadata when present.

Common pattern — only fire if a light was turned on by a person (not by a rule):

[[conditions]]
type = "device_last_change"
device = "living_room.lamp"
kind = "physical"

CalendarActive

Passes when a calendar event is currently active (start ≤ now < end). Reads from the loaded .ics calendars in [calendars].dir.

[[conditions]]
type = "calendar_active"
calendar_id = "us_holidays" # optional — stem of the .ics filename
title_contains = "Holiday" # optional — case-insensitive substring
FieldRequiredDescription
calendar_idnoStem of the .ics filename. Omit to match any calendar.
title_containsnoCase-insensitive substring match on the event title. Omit to match any event.

Combine with a time-based trigger to gate rule execution by calendar state — e.g. "only run the morning lighting routine on workdays" by loading a workdays calendar and adding CalendarActive with title_contains = "Workday".


And

Explicitly groups conditions with AND logic. All wrapped conditions must pass. This is equivalent to listing conditions at the top level (which also AND together), but useful for nesting inside Or or Xor.

[[conditions]]
type = "and"

[[conditions.conditions]]
type = "device_state"
device_id = "mode_night"
attribute = "on"
op = "Eq"
value = true

[[conditions.conditions]]
type = "time_window"
start = "22:00"
end = "06:00"

Or

At least one of the wrapped conditions must pass.

[[conditions]]
type = "or"

[[conditions.conditions]]
type = "device_state"
device_id = "yolink_front_door"
attribute = "open"
op = "Eq"
value = true

[[conditions.conditions]]
type = "device_state"
device_id = "yolink_back_door"
attribute = "open"
op = "Eq"
value = true

Xor

Exactly one of the wrapped conditions must pass (exclusive or).

[[conditions]]
type = "xor"

[[conditions.conditions]]
type = "device_state"
device_id = "switch_away_mode"
attribute = "on"
op = "Eq"
value = true

[[conditions.conditions]]
type = "device_state"
device_id = "switch_vacation_mode"
attribute = "on"
op = "Eq"
value = true

This fires only if exactly one of away mode or vacation mode is active, but not both.


Combining conditions

All top-level conditions AND together by default. Use Or, And, and Xor for compound logic, or use a ScriptExpression for complex expressions:

# OR: fire if EITHER door is open
[[conditions]]
type = "script_expression"
script = '''
device_state("yolink_front_door")["open"] == true ||
device_state("yolink_back_door")["open"] == true
'''

# Complex AND/OR mix
[[conditions]]
type = "script_expression"
script = '''
let night = device_state("mode_night")["on"] == true;
let temp = device_state("thermostat")["temperature"];
night && (temp > 78 || temp < 65)
'''

Condition trace in fire history

Every condition evaluation is recorded in the rule's fire history. For debugging, check:

curl -s http://localhost:8080/api/v1/automations/RULE_ID/history \
-H "Authorization: Bearer $TOKEN" | jq '.[0].conditions'

Each condition entry includes passed, actual, expected, and a human-readable reason.