Skip to main content

Virtual Devices

HomeCore provides three types of software-only devices for building complex automations: timers, switches, and modes. They work exactly like real devices — same MQTT topics, same state bridge, same rule engine integration.

Virtual Switches

A virtual switch is an on/off boolean flag. Use them as software conditions in rules: "away mode", "guest mode", "vacation mode", "morning routine running".

Configuration

Switches are registered like any other device. The plugin ID is switch and the device_id prefix is switch_:

# Example: switch registered by a plugin or created via API
device_id = "switch_away_mode"
name = "Away Mode"

Or they can be managed by the built-in SwitchManager — create devices with plugin_id = "switch" in the registry.

Commanding

# Turn on
curl -s -X PATCH http://localhost:8080/api/v1/devices/switch_away_mode/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "on"}'

# Turn off
curl -s -X PATCH http://localhost:8080/api/v1/devices/switch_away_mode/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "off"}'

# Toggle
curl -s -X PATCH http://localhost:8080/api/v1/devices/switch_away_mode/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "toggle"}'

Both {"command":"on"} and {"on": true} payloads are accepted.

In rules

# Condition: away mode is ON
[[conditions]]
type = "device_state"
device = "mode.away_switch"
attribute = "on"
op = "Eq"
value = true

# Action: turn away mode on
[[actions]]
type = "set_device_state"
device = "mode.away_switch"
state = { command = "on" }

Virtual Timers

A countdown timer device. Supports start, pause, resume, cancel, and reset. When the timer finishes, it fires a DeviceStateChanged event with state = "finished", which rules can react to.

Timer state machine

idle → running → finished
→ paused → running (resume)
→ cancelled → idle (restart)

State values: idle, running, finished, paused, cancelled

Commanding

# Start a 5-minute timer
curl -s -X PATCH http://localhost:8080/api/v1/devices/timer_garage_close/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "start", "duration_secs": 300}'

# Restart with same duration
curl -s -X PATCH http://localhost:8080/api/v1/devices/timer_garage_close/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "restart"}'

# Pause
curl -s -X PATCH http://localhost:8080/api/v1/devices/timer_garage_close/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "pause"}'

# Resume
curl -s -X PATCH http://localhost:8080/api/v1/devices/timer_garage_close/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "resume"}'

# Cancel
curl -s -X PATCH http://localhost:8080/api/v1/devices/timer_garage_close/state \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "cancel"}'

Timer attributes

AttributeDescription
stateCurrent state: idle, running, finished, paused, cancelled
remaining_secsSeconds remaining (only when running or paused)
duration_secsTotal configured duration
started_atISO-8601 timestamp when last started

Reacting to timer completion in rules

name = "Garage close — lights off"

[trigger]
type = "device_state_changed"
device = "garage.close_timer"
attribute = "state"
to = "finished"

[[actions]]
type = "set_device_state"
device = "garage.main_light"
state = { on = false }

Common timer pattern — garage door close reminder

# When garage opens → start 10-minute timer
name = "Garage door — start close timer"
enabled = true

[trigger]
type = "device_state_changed"
device = "garage.main_door"
attribute = "open"
to = true

[[actions]]
type = "set_device_state"
device = "garage.close_timer"
state = { command = "restart", duration_secs = 600 }

---

# When timer finishes → notify
name = "Garage door — close reminder"
enabled = true

[trigger]
type = "device_state_changed"
device = "garage.close_timer"
attribute = "state"
to = "finished"

[[conditions]]
type = "device_state"
device = "garage.main_door"
attribute = "open"
op = "Eq"
value = true

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

---

# When garage closes → cancel the timer
name = "Garage door — cancel timer on close"
enabled = true

[trigger]
type = "device_state_changed"
device = "garage.main_door"
attribute = "open"
to = false

[[actions]]
type = "set_device_state"
device = "garage.close_timer"
state = { command = "cancel" }

Modes

Modes are named boolean flags with optional solar calculation. They are managed by the ModeManager and configured in config/modes.toml.

Mode types

Solar modes: Computed from sunrise/sunset with optional offset. mode_night is the canonical solar mode — on from sunset to sunrise.

Manual modes: Simple on/off boolean flags set via API or rule actions.

Configuration (config/modes.toml)

[[modes]]
name = "mode_night"
type = "solar"
# on_at_offset = 0 # minutes after sunset to turn on (-30 = 30 min before)
# off_at_offset = 0 # minutes after sunrise to turn off

[[modes]]
name = "mode_away"
type = "manual"
default = false

[[modes]]
name = "mode_vacation"
type = "manual"
default = false

[[modes]]
name = "mode_movie"
type = "manual"
default = false

API

# List all modes and their current state
curl -s http://localhost:8080/api/v1/modes \
-H "Authorization: Bearer $TOKEN" | jq

# Set a manual mode
curl -s -X PATCH http://localhost:8080/api/v1/modes/mode_away \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on": true}'

# Adjust solar offset (minutes before/after event)
curl -s -X PATCH http://localhost:8080/api/v1/modes/mode_night \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on_at_offset": -30, "off_at_offset": 15}'

In rules

# Trigger: when mode_night turns on
[trigger]
type = "mode_changed"
mode_name = "mode_night"
to = true

# Condition: is mode_night active?
[[conditions]]
type = "mode_is"
mode_name = "mode_night"
value = true

# Action: set mode from a rule
[[actions]]
type = "set_mode"
name = "mode_away"
value = true

Mode device IDs

Mode devices are accessible as mode_{name}:

# Check mode state
curl -s http://localhost:8080/api/v1/devices/mode_night \
-H "Authorization: Bearer $TOKEN" | jq .attributes.on

Use in DeviceState conditions:

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