Schedule MQTT & Automations
Audience: Firmware, backend, mobile, QA.
One mental model end-to-end: each automation program is one firmware scheduler slot.
| Layer | Shape |
|---|---|
| REST / DB | { mode, time, weekdays, action, … } |
| MQTT | { cmd: "program.set", program: { mode: 1|3|4|5|6|7, … } } |
Mode numbers: see Schedule Modes (SoT). Mode 2 is reserved — never write. Point OFF = mode:1 + relay_status:0 (REST: fixed + action:off). Create auto_off → 400. Max 10 programs.
1 program = 1 program.set. UI “on at 08:00 / off at 22:00” = two fixed programs.
Flow
Mobile / API → POST/PATCH/DELETE automations
↓
DB programs (revision++)
↓
MQTT {mqtt_prefix}/{device_key}/cmd/device
1. program.clear (command_id: sync-clear-{rev})
2. program.set × N (command_id: sync-set-{rev}-{pid}-{nonce})
↓
Device NVS
- REST can succeed even if MQTT publish fails (outbox retry).
- Sync is a full replace:
program.clearthen all enabled programs. - Critical:
program.setmust use a uniquecommand_idevery publish. Reusingrule.idcauses firmware to drop duplicates (no ack → heal/toggle broken).
Topics
| Direction | Topic | Messages |
|---|---|---|
| Cloud → device | {prefix}/{device_key}/cmd/device | program.clear, program.set, manual.set, time.sync, location.set, … |
| Device → cloud | {prefix}/{device_key}/cmd/ack/device | Command acks |
| Device → cloud | {prefix}/{device_key}/state/schedule | Retained program list (twin) |
| Device → cloud | {prefix}/{device_key}/event | e.g. relay_changed |
MQTT wire
program.clear
{ "cmd": "program.clear", "command_id": "sync-clear-3" }
program.set
{
"cmd": "program.set",
"command_id": "sync-set-3-1-a1b2c3d4",
"program": {
"pid": 1,
"mode": 1,
"time": "18:30",
"offset": { "hour": 0, "minute": 0 },
"relay_no": 1,
"relay_status": 1,
"periodic_on_minute": 0,
"periodic_off_minute": 0,
"periodic_cycle_total": -1,
"days": "1111100",
"enabled": 1
}
}
| Field | Notes |
|---|---|
command_id | Unique per set: sync-set-{revision}-{pid}-{nonce} |
pid | Slot among enabled programs |
mode | Write whitelist: 1, 3, 4, 5, 6, 7 |
days | Mon-first mask; API weekdays use Sun=0 … Sat=6 |
Random (mode 7)
| Field | Meaning | Example |
|---|---|---|
time | Window start | "18:00" |
periodic_off_minute | Window length (minutes) | 180 (=21:00) |
periodic_on_minute | Pulse ON duration | 10 |
periodic_cycle_total | Pulse count (1–16) | 4 |
relay_status | ON phase | 1 |
Constraint: on × count ≤ window.
Mobile automations API (moid)
| Method | Path | Notes |
|---|---|---|
GET | /moid/devices/{device_id}/automations | { revision, programs } |
POST | …/automations | { program } |
PATCH | …/automations/{rule_id} | mode immutable |
DELETE | …/automations/{rule_id} |
- Max 10 → 403
mode: "auto_off"→ 400
REST fields for random
| API | UI suggestion |
|---|---|
time | window_start |
periodic_off_minute | window minutes |
periodic_on_minute | on_minute |
periodic_cycle_total | pulse_count |
Example:
{
"program": {
"enabled": true,
"channel_id": "relay_1",
"timezone": "Europe/Istanbul",
"mode": "random",
"weekdays": [0, 1, 2, 3, 4, 5, 6],
"time": "18:00",
"action": "on",
"periodic_on_minute": 10,
"periodic_off_minute": 180,
"periodic_cycle_total": 4
}
}
Reverse sync / heal
Wire mode:2 → REST fixed + action:off. Heal push always encodes OFF as MQTT mode:1. Unique command_id on every set.
| CP empty? | Action |
|---|---|
| Yes + device has programs | Bootstrap import |
| No + equal | No-op |
| No + drift | Keep CP; re-push |
Mistakes to avoid
| Don't | Do |
|---|---|
Wire mode:2 / REST auto_off | fixed + action:off (create rejects auto_off) |
command_id = rule.id | sync-set-{rev}-{pid}-{nonce} |
| Old random = single minute / window-only | Pulse fields + on × count ≤ window |
| >10 programs | Cap at 10 |
Related
- Schedule Modes — mode number SoT
- Device State
- ESP-IDF MQTT topics