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.csvt. It carries no offset, and there is no local-time timestamp anywhere.last_known_state.jsonis 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.jsonimagevalues underimages/device_images/are stock artwork shipped with the app, not uploads; onlyassets/user_images/...paths correspond to a file inimages/.
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, deviceXLQd6aExkzDOkdyDoV0KQer - 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
_1mand 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
.sha256sidecar sits next to the archive; the archive cannot contain its own checksum, so verify withsha256sum -c <name>.zip.sha256