# 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_` | `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; `_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_` | | `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: ```json { "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::` 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_` | | `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. ```sh 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: ```sql -- 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. ```json { "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 .zip.sha256`