Event logging
On-device history of discrete actions (actuator moves, trips, scheduler applies). This is an application contract — the ESP-IDF SDK does not yet ship a log component. Use this page as the target shape if you implement logging now or later fold it into the SDK.
Live MQTT …/event (and BLE type:event) is push: one JSON per occurrence, no backlog. History is pull only via event.get. Do not reconstruct a timeline from retained state or from the live topic.
Occurrence (user / scheduler / alert / …)
│
├─ persist ring in NVS ← history
└─ optional live publish …/event (QoS 1, no retain)
│
▼
Mobile / cloud ── event.get ──► cmd/ack { count, events[] }
Same command body on MQTT and BLE (ch: "device"). No aliases (relay_history.get, event_history.get).
Quick rules
| Command | event.get |
| Channel | ch: "device" |
| Storage | NVS ring; last N records (product default 50; oldest dropped) |
| Order | newest first (events[0] is the latest) |
| Time | RTC UTC civil fields (year…second). Same clock as time.sync. Do not apply local_offset on the device. |
| Pagination | None — one response is the full ring |
| Extra request fields | None (limit / offset not used) |
| BLE | Large JSON may be split with cmd.ack.part (same pattern as other bulky acks) |
Empty log: ok: true, count: 0, events: [].
Request
MQTT — {prefix}/cmd/device
{"cmd":"event.get","command_id":"01KTQ8..."}
Alternative: {prefix}/cmd with "ch":"device" (see MQTT topics).
BLE
{"type":"cmd","ch":"device","cmd":"event.get","rid":"r-ev","command_id":"01KTQ8..."}
If command_id is present on the request, echo it on the ack. BLE also echoes rid.
Response
MQTT: {prefix}/cmd/ack/device · BLE: type: cmd.ack (same fields + rid)
{
"cmd": "event.get",
"ok": true,
"count": 3,
"events": [ { }, { }, { } ],
"ts": 1755603332
}
ts is Unix epoch seconds (ack time, not the age of events[0]).
Errors
ok | message | Meaning | Client |
|---|---|---|---|
false | log_unavailable | Log not initialized | Rare; retry |
false | not_ready / busy | MQTT settle / heap | Short backoff, retry |
Event object
Every record always has:
| Field | Type | Meaning |
|---|---|---|
source | string | Why it happened (closed set below) |
state | bool | Domain “active” flag (example: actuator ON = true) |
year month day hour minute second | number | UTC civil time of the occurrence |
Unknown source values: show as “Other”; do not fail the list.
Recommended source values
Keep the set small and stable so consoles/mobile can localize without a firmware table.
| Value | When | UI hint |
|---|---|---|
user | Operator MQTT/BLE set, or unexplained physical change with no fault | User |
scheduler | A timed/astro/random program applied the output | Scheduler + extra fields |
alert | Protection/fault logic changed the output | Protection + faults |
Products may add sources later (cloud, ble, boot) — treat extra strings as “Other” until documented.
Extra fields by source
Omit unused keys (user has no mode / faults).
scheduler
| Field | Type | Meaning |
|---|---|---|
mode | string | Wire name of the program kind |
active_mode | number | Numeric id for the same kind |
program_id | number | Program id (pid). Sentinel (e.g. 0xFF) if the path was “manual via scheduler” |
Example mode / active_mode map (actuator / timer products):
mode | active_mode | Meaning |
|---|---|---|
manual | 0 | Manual apply through the scheduler path |
schedule | 1 | Fixed clock |
periodic_direct | 3 | Periodic (in-day) |
periodic_time | 4 | Periodic (clock anchor) |
astro_sunrise | 5 | Sunrise |
astro_sunset | 6 | Sunset |
random_window | 7 | Random window |
none | 255 | No mode (rare) |
alert
| Field | Type | Meaning |
|---|---|---|
fault_bits | number | Raw bitmap |
faults | string[] | Active bit names (same keys as fault.get if the product has that command) |
Typical names: over_current, over_voltage, under_voltage, energy_low, reset_overflow.
Example records
{
"source": "user",
"state": true,
"year": 2026, "month": 8, "day": 19,
"hour": 13, "minute": 35, "second": 32
}
{
"source": "scheduler",
"state": false,
"year": 2026, "month": 8, "day": 19,
"hour": 11, "minute": 46, "second": 0,
"mode": "schedule",
"active_mode": 1,
"program_id": 1
}
{
"source": "alert",
"state": false,
"year": 2026, "month": 8, "day": 19,
"hour": 12, "minute": 2, "second": 11,
"fault_bits": 1,
"faults": ["over_current"]
}
Live event vs history
Live …/event | event.get | |
|---|---|---|
| Purpose | Instant notify (fault, relay_changed, …) | On-device ring |
| Transport | MQTT topic / BLE notify | Command ack |
| Completeness | Missed while offline | Survives reboot (NVS) |
| Client | Optional toast / twin update | History screen |
Opening a history UI: call event.get once. Live twin / relay_changed does not append into that list. If the screen stays open, call event.get again after a new move.
Fault SET/CLEAR timelines (if any) stay on a separate command (e.g. fault.get). This log is output/action history, not the full diagnostic journal.
What not to store
- Boot baseline read (first sample after power-on)
- Echo of a SET you just applied (MCU/cloud round-trip) — one row per real change
- Moves while the clock is unsynced (e.g.
year < 2020)
Client UX
- On history screen: one
event.get. - Render
eventsas-is (already newest-first). - Row: convert UTC civil fields to device TZ + ON/OFF (
state) + reason (source). - Scheduler rows: show
mode/program_id. Alert rows: showfaults. - BLE: reassemble
cmd.ack.partbefore parsingevents.
Implementation notes (future SDK)
Not in iotmer today. A later component would likely provide:
- Ring in NVS (fixed record size, capacity Kconfig)
iotmer_event_log_append()from the app (source + state + optional extras)event.getdispatch helper sharing MQTT and BLE JSON- Optional live publish hook (
iotmer_event/ existing…/event)
Until then: keep JSON keys stable so mobile and console can ship against this contract.
Related: Telemetry, MQTT topics, BLE operational protocol.