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

516 lines
25 KiB
Markdown

# 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:
```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:<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.
```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 <name>.zip.sha256`