Skip to main content

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

Commandevent.get
Channelch: "device"
StorageNVS ring; last N records (product default 50; oldest dropped)
Ordernewest first (events[0] is the latest)
TimeRTC UTC civil fields (yearsecond). Same clock as time.sync. Do not apply local_offset on the device.
PaginationNone — one response is the full ring
Extra request fieldsNone (limit / offset not used)
BLELarge 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

okmessageMeaningClient
falselog_unavailableLog not initializedRare; retry
falsenot_ready / busyMQTT settle / heapShort backoff, retry

Event object

Every record always has:

FieldTypeMeaning
sourcestringWhy it happened (closed set below)
stateboolDomain “active” flag (example: actuator ON = true)
year month day hour minute secondnumberUTC civil time of the occurrence

Unknown source values: show as “Other”; do not fail the list.

Keep the set small and stable so consoles/mobile can localize without a firmware table.

ValueWhenUI hint
userOperator MQTT/BLE set, or unexplained physical change with no faultUser
schedulerA timed/astro/random program applied the outputScheduler + extra fields
alertProtection/fault logic changed the outputProtection + 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

FieldTypeMeaning
modestringWire name of the program kind
active_modenumberNumeric id for the same kind
program_idnumberProgram id (pid). Sentinel (e.g. 0xFF) if the path was “manual via scheduler”

Example mode / active_mode map (actuator / timer products):

modeactive_modeMeaning
manual0Manual apply through the scheduler path
schedule1Fixed clock
periodic_direct3Periodic (in-day)
periodic_time4Periodic (clock anchor)
astro_sunrise5Sunrise
astro_sunset6Sunset
random_window7Random window
none255No mode (rare)

alert

FieldTypeMeaning
fault_bitsnumberRaw bitmap
faultsstring[]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 …/eventevent.get
PurposeInstant notify (fault, relay_changed, …)On-device ring
TransportMQTT topic / BLE notifyCommand ack
CompletenessMissed while offlineSurvives reboot (NVS)
ClientOptional toast / twin updateHistory 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

  1. On history screen: one event.get.
  2. Render events as-is (already newest-first).
  3. Row: convert UTC civil fields to device TZ + ON/OFF (state) + reason (source).
  4. Scheduler rows: show mode / program_id. Alert rows: show faults.
  5. BLE: reassemble cmd.ack.part before parsing events.

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.get dispatch 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.