Files
Shelly-Loqed-Touch/lock_data/after_init/AGENTS.md
T
2026-10-27 07:56:02 +01:00

25 KiB

Machine-readable notes for this export

Companion to README.md, which says the same things in plain language for the person this data belongs to. This file is the precise version: complete reference tables, exact types, and the places where a careless reading goes wrong.

If you are an assistant helping someone read this folder, read this file first.

Three things that will make you wrong

1. p in the event log is not self-describing. It is a positional JSON array. The key order is not in the data — it is in the per-code tables below. Do not guess field names from the values.

2. e and event_id are unrelated numbering schemes. e in the event log says what happened. event_id in the statistics says what is being measured. They collide and mean different things:

Number e (event log) event_id (statistics)
1 Switched on or off Switch or relay with energy metering
4 A battery-powered device woke up and reported Temperature and humidity sensor
8 Brightness changed Door, window or position sensor
12 Smoke alarm Temperature sensor
13 An input (switch or button) changed Gas or air-quality sensor
16 A Bluetooth temperature and humidity reading Analogue input

Never resolve one against the other.

3. Absence is not nonexistence. Whole categories were removed from this export by policy, listed under "Excluded" below. If something is not here, that says nothing about whether the account used it. Do not conclude the owner had no automations.

Identity

One physical device, written three ways:

Form Value Appears in
decimal XLQd6aExkzDOkdyDoV0KQer statistics device_id
hex XLQd6aExkzDOkdyDoV0KQer device.json id of channel 0, event tag
hex + channel XLQd6aExkzDOkdyDoV0KQer_<n> device.json ids, event tag

Conversion: parseInt("XLQd6aExkzDOkdyDoV0KQer", 16) === XLQd6aExkzDOkdyDoV0KQer. Device ids beginning with X are not numeric and have no hex form; they appear verbatim everywhere.

Channels. 1 channel(s). The bare hex id is channel 0; <hex>_N is channel N. Each is a separate record with its own name, room and image, but all are one physical unit.

id channel name
XLQd6aExkzDOkdyDoV0KQer 0 Back door

Rooms. room_id occurs in device.json and in the statistics as a bare integer. Only the ids this bundle's own records use are resolved here; the home's full room list is out of scope:

room_id name
2 Lab

room_id records the assignment at write time, so one channel can carry several across its history and a row does not tell you where the device is now. An id with no name could not be resolved against the home's current rooms - it may be deleted, or a sentinel written before assignment (-10 occurs on a device's first day). Do not infer which. It is listed rather than dropped so that no integer in the data is left unexplained, and the same applies to tariff_id: a negative value there is a placeholder, not a tariff you can look up.

Joining the files

event_log.tag → channel number → statistics.channel:

XLQd6aExkzDOkdyDoV0KQer      -> channel 0
XLQd6aExkzDOkdyDoV0KQer_N    -> channel N

A suffix is dependable; the absence of one is not. Across seven houses and ~120,000 events a _N suffix never disagreed with the payload channel. That is an observation, not a guarantee — treat it as a strong default, not an invariant to assert on. A bare tag usually means channel 0 but sometimes does not — Cury devices (event code 21) keep a bare tag and put the channel only in the payload, and log entries written before roughly 19 December 2022 do the same for other codes, from before the suffix existed.

So: where the payload carries a channel, prefer it over the tag. Fall back to the tag suffix only for codes whose payload has no channel field. Getting this backwards silently files one channel's history under another.

Payload position 1 is the channel for codes 1, 2, 5, 8, 11, 12, 13 and 21 only. For every other code that position is something else entirely — reason for code 4, scnid for code 6 — so do not read it as a channel there.

A component reference is not a number, but it does join. Add-on and virtual components report channel as a reference — enum:200, boolean:201, number:200 — mostly under codes 17 and 22, and their tag carries no _N suffix. Coercing the reference itself to an integer produces nonsense, but the numeric part after the colon is the statistics channel: number:200 joins to channel 200. Split on the colon rather than discarding the row. Confirmed against a real export.

Times do not join directly. Event t is an integer of milliseconds since the Unix epoch; statistics _time is an ISO 8601 string in UTC marking the start of the period the row covers. Convert before comparing.

Inventory

File Rows Bytes Selected by
device.json 1 channel(s) — hex id and XLQd6aExkzDOkdyDoV0KQer_<n>
event_log.csv 4 — event tag
last_known_state.json 3 key(s) — DEV:XLQd6aExkzDOkdyDoV0KQer:*
images/ 0 file(s) — image field of each channel

The same inventory, for a tool rather than a reader. This device produced no statistics at all, which the empty files list states rather than implies:

{
  "schema": "shelly-device-export/2",
  "event_log": {
    "rows": 4,
    "matched": 4,
    "withheld": 0,
    "cap": 5,
    "cap_scope": "device",
    "per_tag": {
      "XLQd6aExkzDOkdyDoV0KQer": 4
    }
  },
  "files": []
}

Schemas

last_known_state.json

The device's own view of itself, as the platform last cached it. Three sources, each a Redis hash under DEV:<decimal-id>: in the per-server database that holds device state:

Member Source key Holds
info INFO identity and connection metadata — id, id16, type, code, gen, online, conntime, ip
status STATUS last reported component state, one member per component (switch:0, input:1, wifi, …)
settings SETTINGS device configuration as its own firmware reports it

Hash field values are JSON where they parse and the raw string where they do not — code is a bare model string, wifi is an object. Parse defensively. Which of the three are present, which were not there at all, and which values were blanked are all stated as data: absent and redacted in the file, and the Provenance section below.

[redacted] marks a value removed because its key is secret-shaped. It means the field existed and held something; a field that was empty is left as it was and is not listed in redacted, so the list never implies a secret that never was.

event_log.csv

Column Type Notes
tag string XLQd6aExkzDOkdyDoV0KQer or XLQd6aExkzDOkdyDoV0KQer_<n>
t integer milliseconds since the Unix epoch, UTC
e integer event code; see the full table below
p JSON string positional array

Rows: 4 of 4 matched, cap 5. Source table event_logs (version 1). Range: 2026-10-21T07:10:53.259Z to 2026-10-21T07:22:58.358Z - the rows in this file. Codes present: 18. That list describes the rows this file carries, which may be a subset of what the device produced - see Provenance for the counts. A code absent from it is not evidence the device never produced one.

Sentinels and traps

  • Every timestamp in the data files is UTC - event_log.csv t. It carries no offset, and there is no local-time timestamp anywhere. last_known_state.json is the exception worth knowing about: it carries the device's own clock configuration - a zone name, a UTC offset in seconds, and a wall-clock string like "15:50". Those describe the device, not the data, and must not be used to reinterpret a timestamp that is already UTC.
  • device.json image values under images/device_images/ are stock artwork shipped with the app, not uploads; only assets/user_images/... paths correspond to a file in images/.

Loading into SQLite

The most practical way to work with this, and it keeps the data on the machine it is already on. Free GUI clients (DB Browser for SQLite, DBeaver) open the result.

sqlite3 device.db
.mode csv
.import event_log.csv ev

.import types every column TEXT. Numeric work needs an explicit cast, or sum() silently returns 0:

-- decode the event log: positional payload and epoch, in one pass
select datetime(cast(t as integer)/1000, 'unixepoch') as when_utc,
       json_extract(p, '$[0]') as device,
       json_extract(p, '$[1]') as channel,
       json_extract(p, '$[2]') as value
from ev order by cast(t as integer);

Data quality: what you may legitimately see

This is an export of what is stored, not a cleaned or corrected version. Some of it will look wrong, and usually is wrong at source rather than in transit:

  • A device can be dead and still attached to the account. It may keep reporting implausible values, or stop reporting while still appearing in device.json. A gap in the statistics is not evidence the device was off.
  • Firmware and platform changes alter what a device reports over the years these files cover. A field can change meaning, units, or stop being written, without anything in the data marking the change.
  • Out-of-range readings are common — a temperature of hundreds of degrees, an energy spike, a battery above 100. These are what the device sent.

If you are summarising this for someone: do not silently repair it. Do not interpolate gaps, drop outliers without saying so, or average across a discontinuity. Report anomalies as anomalies — an average that quietly includes a nonsense spike is worse than no average, and the person reading it cannot tell the difference.

Reference: all event-log codes

The complete set the platform defines, independent of what this bundle contains - so you can tell a code that is absent from this export from one that does not exist. Which codes occur here is stated as data, in Codes present: above. keys is the order of values in a positional payload.

This reference reflects the platform as of 2026-10-23. The services that write this data change. A code may have been added, or a payload reshaped, since this export was produced. Where the data disagrees with the table below, the data is what we hold — trust it and say the table looks out of date.

e name meaning payload order
1 onOff Switched on or off deviceId, channel, turn
2 overPower Power limit exceeded deviceId, channel
3 overTemperature Temperature limit exceeded deviceId, tmp
4 wakeUp A battery-powered device woke up and reported deviceId, reason, tmp, bat, alarm_hum
5 openCloseStop A blind or shutter opened, closed or stopped deviceId, channel, state, current_pos
6 sceneExec An automation ran tstamp, scnid
7 DwState A door or window opened or closed deviceId, reason, state, bat, lux_value, lux_illumination, tilt, vibration, tmp
8 brightness Brightness changed deviceId, channel, brightness
9 gasSensor Gas alarm changed deviceId, alarm_gas
10 motion Motion was detected deviceId, motion, bat, lux_value, lux_illumination, tmp
11 gen2Errors The device reported an error deviceId, channel, error, ...error-specific values
12 smokeAlarm Smoke alarm deviceId, channel
13 inputState An input (switch or button) changed deviceId, channel, state, component
14 DwBleState A Bluetooth door or window sensor opened or closed deviceId, reason, state, bat, lux, tilt
15 MotionBleState A Bluetooth motion sensor detected motion deviceId, motion, bat, lux_value
16 HnTBLEState A Bluetooth temperature and humidity reading deviceId, tmp, hum, bat, lux_illumination
17 xModGeneric A reading from an add-on or Shelly X module deviceId, ref, value
18 lockState A lock was locked or unlocked deviceId, state
19 vibrationDistanceBatState A vibration or distance reading deviceId, state, distance, bat
20 presence Presence was detected deviceId, ref, state, num_objects
21 cury A Cury device changed state deviceId, channel, state, level
22 camera A camera detected motion deviceId, zone, motion, rec_id, media_id
23 breakerRPC A circuit breaker changed state deviceId, state, reason
24 cameraStatus A camera arm or privacy setting changed deviceId, arm, privacy

Payload key meanings:

Key Meaning
deviceId the device this event came from
channel which output or channel of the device
turn true = switched on, false = switched off
tmp temperature, in degrees Celsius
hum relative humidity, in percent
bat battery level remaining, in percent
reason what prompted the report
state the new state
current_pos position, in percent
tstamp when it happened, in milliseconds since 1 January 1970 (UTC)
scnid the identifier of the automation that ran
lux light level
lux_value measured light level
lux_illumination light level as a description (for example dark or bright)
tilt tilt angle
vibration whether vibration was detected
brightness brightness, in percent
alarm_gas gas alarm state
alarm_hum humidity alarm state
motion whether motion was detected
error the error the device reported
component which part of the device (for example an input)
ref which sub-component reported
value the reported value
distance measured distance
num_objects how many were detected
level the reported level
zone which camera zone
rec_id recording identifier
media_id media identifier
arm whether the camera was armed
privacy whether privacy mode was on

Caveats worth carrying: the payload of code 4 varies by device model (a Gen2 smoke sensor writes [deviceId, reason, battery]), and code 11 appends error-specific values after error.

Reference: measurement families

Every family this tool can export, independent of what this bundle contains, so an absent event_id can be told from an undocumented one. This tool reads statistics at the raw, _1m, _1h and _1d resolutions, and from the net metering, monthly and archived-meter stores. Which of those appear in a given bundle depends on the scope of the export, on what the device wrote, and on the options it ran with - the file inventory above and Provenance below are what say. One thing is written by the platform but falls outside that scope and is in no bundle:

  • overall_consumption - Whole-platform consumption totals. These carry no tags at all, not even a home id, so no part of them belongs to any one account.

This reference reflects the platform as of 2026-10-23. Families may have been added, or fields reshaped, since this export was produced. Where the data disagrees with the table below, the data is what we hold.

Families present in this bundle: none.

event_id measurement fields
1 Switch or relay with energy metering f1 channel number; f2 mains voltage, in volts; f3 energy used during the period, in watt-minutes - divide by 60000 for kwh; f4 energy returned to the grid during the period, in watt-minutes - divide by 60000 for kwh
4 Temperature and humidity sensor f2 relative humidity, in percent; f3 lowest humidity during the period, in percent; f4 highest humidity during the period, in percent; f5 lowest temperature during the period, in degrees celsius; f6 highest temperature during the period, in degrees celsius
8 Door, window or position sensor f2 position, in percent; f3 battery level remaining, in percent; f5 lowest temperature during the period, in degrees celsius; f6 highest temperature during the period, in degrees celsius
10 Energy meter f1 channel number; f3 energy used during the period, in watt-hours - divide by 1000 for kwh; f4 energy returned to the grid during the period, in watt-hours - divide by 1000 for kwh; f5 lowest voltage during the period, in volts; f6 highest voltage during the period, in volts
12 Temperature sensor f1 channel number; f3 lowest temperature during the period, in degrees celsius; f4 highest temperature during the period, in degrees celsius
13 Gas or air-quality sensor f1 measured concentration; f2 measured concentration
14 Humidity sensor f1 channel number; f3 lowest humidity during the period, in percent; f4 highest humidity during the period, in percent
15 Voltage measurement f3 lowest voltage during the period, in volts; f4 highest voltage during the period, in volts
16 Analogue input f3 lowest reading during the period, in percent; f4 highest reading during the period, in percent
17 Counter or accumulated total
18 Frequency, presence, soil or other generic sensor
19 Generic counter counter accumulated count
20 Weather station temperature temperature, in degrees celsius; min_temperature lowest temperature during the period, in degrees celsius; max_temperature highest temperature during the period, in degrees celsius; humidity relative humidity, in percent; pressure air pressure; min_pressure lowest air pressure during the period; max_pressure highest air pressure during the period; illuminance light level, in lux; precipitation rainfall during the period; uv ultraviolet index; wind_speed wind speed
21 Air-quality sensor co2 carbon dioxide concentration, in ppm; humidity relative humidity, in percent; min_temperature lowest temperature during the period, in degrees celsius; max_temperature highest temperature during the period, in degrees celsius; min_pressure lowest air pressure during the period; max_pressure highest air pressure during the period

Reference: machine-readable summary

The parts of this document a tool should act on rather than read: the scale factors, what makes a statistics row unique, the event log's per-tag cap, and the defects the data is known to carry. Every value here is generated from the same constants that produced the prose above, so the two cannot disagree. It states no retention window for the statistics, deliberately - see Expired below.

A unit stated in English can be confidently wrong in a way a scale factor cannot - these documents described the energy columns in the wrong unit for months, and every sentence saying so agreed with every other. to_kwh is the number that settles it. Read it per family. f3 does not hold the same unit everywhere: a relay writes watt-minutes and an energy meter writes watt-hours, which is a factor of 60 between two columns of the same name. Match the families list against a row's event_id rather than taking the first entry.

This describes the platform, not this bundle: it is identical in every export. The measured figures for this one are in the Inventory block above.

{
  "schema": "shelly-device-export/2",
  "columns": {
    "f3": [
      {
        "families": [
          1
        ],
        "unit": "W·min",
        "to_kwh": 0.000016666666666666667
      },
      {
        "families": [
          10
        ],
        "unit": "W·h",
        "to_kwh": 0.001
      }
    ],
    "f4": [
      {
        "families": [
          1
        ],
        "unit": "W·min",
        "to_kwh": 0.000016666666666666667
      },
      {
        "families": [
          10
        ],
        "unit": "W·h",
        "to_kwh": 0.001
      }
    ],
    "energy_price": {
      "scale_of": "f3",
      "to_account_currency": [
        {
          "families": [
            1
          ],
          "factor": 0.000016666666666666667
        },
        {
          "families": [
            10
          ],
          "factor": 0.001
        }
      ],
      "currency": null,
      "currency_applies_to_rows": false,
      "currency_at_export_in": "provenance"
    }
  },
  "grain": [
    "_time",
    "channel",
    "room_id",
    "tariff_id"
  ],
  "event_log_retention_per_tag_upstream": 100,
  "known_gaps": [
    {
      "id": "daily-rollup-missing-2300-utc",
      "summary": "`_1d` rows for days before 2026-09-23 omit the 23:00 UTC hour; the rollup was corrected on that date and rows from it are whole, so adding the hour back to a later row double-counts it.",
      "families": null
    },
    {
      "id": "f3-net-or-gross",
      "summary": "`f3` is net of returned energy on some device families and gross on others, with nothing in the row to say which, so it can be negative.",
      "families": [
        1,
        10
      ]
    }
  ]
}

Excluded — absence here means nothing

Five distinct reasons produce a gap in this bundle. None of them is evidence about the owner or the device, and they are not interchangeable:

1. Out of scope — held by the platform, deliberately not in a device export.

  • automations and scenes, including scene-execution events
  • dashboards, groups, room definitions
  • account-level records: identity, contact details, locale and display preferences, billing data
  • third-party integrations (for example Google Home sync)
  • account ownership, access rights, server placement
  • whole-house monthly energy reports
  • every other device in the same home

This bundle is scoped to one device. It should contain no other device identifier. Do not conclude the owner had no automations, or that the home had one device.

2. Expired — deleted by retention, unavailable to anyone. Each store keeps rows for a window and the platform deletes them past it.

The windows are not stated here, and should not be inferred. They are configured per server and are not uniform across the fleet - on at least one host the per-minute store is deliberately longer than elsewhere. A figure printed in this document would therefore be wrong for some bundles, which is exactly what happened before it was removed: a real export understated its own per-minute window by 59 days. Use the measured first_period and last_period in the Inventory block for what this bundle actually covers, distinct_periods against that span for how much of it is present, and do not prune a query against an assumed retention.

The event log is trimmed by count, not age: roughly the newest 100 records per tag, older ones deleted as new arrive, so a busy channel reaches back less far than a quiet one on the same device. A short file is evidence of that trimming, not of inactivity, and the earliest row in a file is a trimming boundary rather than a device's first day.

3. Withheld — held by the platform, not included in this copy. Unlike reason 2, these records exist right now. Nothing has deleted them and a further request can produce them.

No event-log records were withheld from this export: every record matching the device is in the file.

4. Never versioned — stored as a current setting, with no history. Tariff names and rates, and the account currency, are each a single value overwritten on change. No history exists, so no snapshot of them could be attributed to a historical row and none is included. What is attributable is energy_price, computed per row at write time; energy_price / f3 recovers the rate that applied then. The currency in force when this export ran is in Provenance and applies to that moment only.

5. Operational artefacts — the platform's data, not the subject's. device ACTIONS (Gen1 webhook configuration) and CACHE (BLE cloud-relay bookkeeping), and stock artwork shipped with the app.

Provenance

  • Generated 2026-10-23T07:03:32.760Z
  • Time zone: Europe/Vienna (from the home) - every timestamp in the data files is UTC regardless
  • Built from splitty b669c97 (packed)
  • Home 2531873, device XLQd6aExkzDOkdyDoV0KQer
  • Event log from event_logs (version 1)
  • Account tier at export time: free
  • Device state from DEV:XLQd6aExkzDOkdyDoV0KQer:* (local Redis db 0): INFO, STATUS, SETTINGS present
  • Secret-shaped values blanked in last_known_state.json: 0
  • Export scope: limited - the platform's 16 _1m and 14 raw series (its whole sub-hourly inventory, whatever subset of it this device writes) were not queried
  • Event log: all 4 matching records included (cap 5, not reached)
  • A .sha256 sidecar sits next to the archive; the archive cannot contain its own checksum, so verify with sha256sum -c <name>.zip.sha256