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
| Field | Description |
|---|---|
device | Preferred device reference: canonical name, unique display name, or raw device ID |
device_id | Backward-compatible alias for device |
attribute | Attribute name |
op | Comparison operator |
value | Expected value |
Operators (op):
| Operator | Meaning |
|---|---|
Eq | Equal to |
Ne | Not equal to |
Gt | Greater than |
Gte | Greater than or equal to |
Lt | Less than |
Lte | Less 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:
| Function | Returns | Description |
|---|---|---|
device_state("device_id") | map | Current attributes of a device |
hour() | int | Current hour (0-23, local time) |
minute() | int | Current minute (0-59) |
weekday() | int | Day of week (0=Sun, 1=Mon, …, 6=Sat) |
is_weekday() | bool | True if Mon-Fri |
is_weekend() | bool | True 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?"
| Field | Required | Description |
|---|---|---|
mode_id | yes | Mode device id (e.g. "mode_night"). |
on | yes | true 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
| Field | Required | Description |
|---|---|---|
name | yes | Hub variable name. |
op | yes | A CompareOp value: Eq, Ne, Gt, Gte, Lt, Lte, Contains, In. |
value | yes | Comparison 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
| Field | Required | Description |
|---|---|---|
device_id (alias device) | yes | Device whose last change to inspect. |
kind | no | Filter on change kind. |
source | no | Exact match on the source label (plugin id, rule id, etc.). |
actor_id / actor_name | no | Exact 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
| Field | Required | Description |
|---|---|---|
calendar_id | no | Stem of the .ics filename. Omit to match any calendar. |
title_contains | no | Case-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.