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 '{"on": true}'

# 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 '{"on": false}'

# 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 '{"action": "toggle"}'

These are homeCore's two command shapes: a write to the writable on attribute, or a declared action, {"action": "toggle"}. (The retired {"command": "on"} form is still taken from older automations.)

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 = { on = true }

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 '{"action": "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 '{"action": "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 '{"action": "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 '{"action": "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 '{"action": "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 = { action = "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 = { action = "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