---
title: "Device History and Energy · RUAL Documentation"
description: "Why the node records what changed rather than what was said, buckets versus raw readings, meter deltas and the energy view."
canonical: https://docs.rual.nl/home-automation/history-and-energy
language: en
---

# Device History and Energy

The node records what CHANGED rather than what was said, at a resolution chosen per capability. That policy is what makes device history a feature instead of a disk-filling support ticket, and it decides how you read the data back.

A dozen sensors in a normal house produce tens of reports a minute. A motion sensor re-announces while somebody is in the room, a power meter reports every ten seconds, and every Zigbee message carries a link-quality stamp. Storing one row per report is a few million rows a month whose information content is nearly zero.

So nothing here stores what a device *said*. It stores **what changed**, at a resolution chosen per capability. Measured against a twelve-device house: roughly 40 reports a minute in, roughly 25 rows an hour out.

### Every Capability Has a Kind, and the Kind Decides Everything

The organising idea is that a value's *behaviour over time* determines its storage policy, its chart and its aggregation, and all three can be derived from one attribute instead of special-cased in every consumer.

| Kind | Examples | Stored | Charted as | Aggregates to |
| --- | --- | --- | --- | --- |
| **alarm** | occupancy, contact, water leak, smoke, gas, vibration, tamper, battery low, door position, security alarm | On transitions only. A door open for eight hours is two rows, not four hundred and eighty. | Timeline | Fraction of the window it was true |
| **measure** | temperature, humidity, pressure, illuminance, CO₂, VOC, PM2.5, power draw | On change beyond a deadband, plus a heartbeat. | Line | min, max, average, last |
| **meter** | energy in kWh | At a fixed interval, not on change. | Bars | Delta: consumption in that window |
| **state** | power, brightness, colour temperature, position, HVAC mode, fan mode, lock state, target temperature | On change. | Steps | min, max, average, last |
| **event** | button presses, doorbell rings, access entries, NFC swipes, camera motion | Every occurrence, because every occurrence is the data. | Not charted | Count |
| **diagnostic** | battery percentage, link quality | Sparsely: a wide deadband and a long heartbeat. | Line, not by default | min, max, average, last |

Two of those rows are the interesting ones.

**A meter is not a measurement.** Energy in kWh is a monotonic lifetime counter, so charting the raw value is a meaningless upward ramp and "on change" means "on every report". It is sampled at a fixed five minutes instead, because evenly spaced points are what make bucket deltas comparable, and the useful view is consumption per hour or per day rather than the ever-rising total.

**A setpoint is not a measurement either.** `target_temperature` is a state somebody chose: it steps rather than drifts, so it is charted as steps and stored on change.

### Deadbands and Heartbeats

The **deadband** is the smallest change worth recording, in the capability's own unit. It is the single most important number in this design: a temperature sensor reporting 21.03, 21.04, 21.03 every thirty seconds produces 2,880 rows a day that say nothing, and a deadband of 0.1 collapses that to the handful of readings where the room actually changed.

The **heartbeat** is the longest gap allowed between stored points even when nothing changed. Without it a deadband makes a stable sensor indistinguishable from a dead one, because both produce no rows. With it, a flat line is visibly flat and a gap is visibly a gap.

| Capability | Deadband | Heartbeat | Why that number |
| --- | --- | --- | --- |
| `temperature` | 0.1 °C | 15 min | Below the accuracy of the sensors this runs against. |
| `humidity` | 1 % | 15 min | Charted 0 to 100 regardless of the data range, because "is this normal" is the question being asked. |
| `pressure` | 0.5 hPa | 30 min | Weather moves slowly. |
| `illuminance` | 5 lx | 15 min | Lux spans five orders of magnitude between a dark room and direct sun, so a fixed deadband is wrong at one end. 5 keeps the dark end useful, which is the end automations care about. |
| `co2` | 10 ppm | 15 min |  |
| `voc` | 5 ppb | 15 min |  |
| `pm25` | 1 µg/m³ | 15 min |  |
| `power_watts` | 5 W | 5 min | Watts, not percent: the interesting events are appliances switching on and off. A kettle is 2000 W and a standby LED is 0.3 W, and 5 W separates "something happened" from meter jitter. |
| `energy_kwh` | fixed 5 min interval |  | See above: change is not a signal on a counter that always rises. |
| `brightness` | 1 % |  | State, stored on change. |
| `color_temp` | 50 K |  | Below what anyone sees. |
| `battery` | 1 % | 24 h | A battery moves a few percent a week. "When did this start dropping" is a real question; a daily heartbeat answers it. |
| `link_quality` | 20 | 6 h | Recorded because "this sensor stopped working" is nearly always a mesh problem and the shape of the decline is the evidence. Wide deadband: LQI is noisy by tens between consecutive reports. |

Alarms, states with no deadband, enum states and button events have neither: they store on every change, and events store on every occurrence.

**The comparison is against the last STORED value, not the last seen one.** Comparing against the last seen value would let a slow drift through one increment at a time and never record anything.

### What Is Recorded at All

- **Only adopted devices.** A coordinator hears every device ever paired to its network; recording all of it would fill the store with the history of hardware nobody uses.

- **Only capabilities that opt in.** A capability has to be listed in the metadata table with recording switched on. Anything absent records nothing, so adding a capability to the vocabulary cannot silently start filling a disk. `color_hex` is the one capability in the vocabulary that is deliberately not recorded.

- **Booleans are stored as 0 and 1**, not as text, so one numeric column serves both timeline and line charts, and "how long was this true" is an ordinary integral rather than a string parse. Enum states such as `hvac_mode` are stored as text.

- **The access log is recorded but not charted.** Doorbell rings, entry decisions, NFC swipes and camera motion are events, so every occurrence is stored. Who came in, when and how is exactly the history somebody wants, and it is a table rather than a curve. See [the access flow](https://docs.rual.nl/home-automation/example-flows#access) for writing a queryable log alongside it.

This store is separate from the last-known-state cache in Redis that survives a restart. The two answer different questions: the cache holds one current value per capability so a restart does not re-fire every active trigger, and this store holds the trail of how that value got there. See [What Survives a Restart](https://docs.rual.nl/home-automation/devices#persistence).

### How It Is Written

The recorder never writes on the ingest path. It appends to an in-memory buffer, and a background flush writes the batch every 30 seconds by default, or immediately when the buffer reaches 500 readings, because a burst is exactly when the data is interesting. Batching matters far more than the interval: 25 rows in one write beats 25 writes.

A clean shutdown flushes what is buffered, so a restart does not leave a half-minute hole in every chart that spans it. A *failed* write is dropped with a warning rather than retried: analytics is the least important thing this node does, and a retry queue that grows while the store is unhealthy is how a nice-to-have takes down the automation that actually matters.

Reads fold the buffer back in, so a chart opened seconds after a sensor fired shows that reading rather than waiting up to a flush interval. That matters because the single most common use of a history screen is looking at something that just happened.

### Reading It Back

[`get device history`](https://docs.rual.nl/block-types/home/homeassist_device_history) is the only block that reads the analytics store rather than the live registry.

| Pin | Meaning |
| --- | --- |
| `device`, `capability` | What to read. The device resolves by id, native id or exact name, same as every other block. |
| `from`, `to` | The window. Empty means the last 24 hours, which is not arbitrary: the store records changes rather than samples, so a day of a quiet sensor is a handful of rows and a day is the span somebody means by "what has this been doing". Wiring the two the other way round swaps them rather than erroring. |
| `buckets` | 0 returns raw readings only. Greater than 0 additionally returns that many aggregated windows. |

| Out-pin | Carries |
| --- | --- |
| `readings` | The stored readings, oldest first: `device_id`, `capability`, `value`, `text`, `at`. |
| `buckets` | Aggregated windows: `at`, `min`, `max`, `avg`, `last`, `delta`, `count`. |
| `count` | How many raw readings came back. |
| `recording` | False when this node stores no history for that capability. |
| `unit` | The display unit, for example `°C` or `kWh`. |

**Check `recording` before you interpret an empty result.** Empty with `recording = true` means nothing happened in that window. Empty with `recording = false` means this node was never going to tell you: either analytics is switched off, or that capability is not one that is stored. Those are different answers and a dashboard that conflates them lies.

[get device history](https://docs.rual.nl/block-types/home/homeassist_device_history) reads the analytics store, which records what CHANGED rather than every report. With from and to left empty the window is the last 24 hours, and buckets set to 24 additionally returns one aggregated window per hour on the buckets pin. Watch the recording pin: it is false when this node stores no history for that capability, which is a different answer from "nothing happened".

![Studio canvas example for the get device history block: a day of temperature, bucketed for a chart.](https://docs.rual.nl/canvas-examples/homeassist_device_history.png)

### Buckets Versus Raw Readings

Raw readings are the record: exactly what was stored, when. Buckets are for drawing. Ask for both when you are charting, because they answer different questions and re-deriving one from the other in the browser means shipping the aggregation rules to a second place where they can drift.

The bucket shape follows the capability's kind, which is the entire reason kinds exist:

| Kind | The pin that matters | Why |
| --- | --- | --- |
| meter | `delta` | Consumption in that window: this bucket's last value minus the previous bucket's last value. The raw lifetime total charts as a meaningless ramp. |
| alarm | `avg` | The fraction of the window the value was true, which renders as a duty-cycle band. "The hallway was occupied 40% of that hour." |
| everything else | `min`, `max`, `avg`, `last` | The ordinary summary of a gauge. |

`delta` is only emitted for meters. Emitting a zero on every other kind would invite a chart that plots it.

**Empty windows are omitted, not zero-filled.** A gap in a temperature chart means "the sensor did not report", and drawing it as 0 °C invents a cold snap. If your chart library wants a dense series, fill the gaps on the client where you can choose how to render them, rather than having the platform invent readings.

**A meter that goes backwards contributes nothing.** A negative delta means the counter reset: a device replaced, or firmware reflashed. Reporting a large negative consumption would poison every total above it, so that one bucket reports no delta rather than a wrong one.

#### Reading It Over HTTP

```
curl -s 'https://<node>/_system/homeassist/devices/zigbee:0x00158d0007e1a2b3/history?capability=temperature&buckets=24' \
  -H 'Authorization: Bearer <token>'
```

The response carries the readings, the buckets, and a `meta` object with the capability's kind, label, unit, decimals and chart type. That last part is the presentation contract: units, decimals, axis bounds and chart type come from the platform, so a chart cannot drift from what the platform actually records.

It also carries `truncated`. The window defaults to the last 24 hours and the reading count is capped; a chart that silently truncated would be worse than one that shows fewer points, so the response says which happened. `from` and `to` are unix seconds, `buckets=0` asks for raw only.

### Energy

Energy is the capability the meter kind was built for, and the shape of the answer is different from every other reading in this section.

A metering plug reports two things: `power_watts`, the instantaneous draw, and `energy_kwh`, the lifetime counter. They answer different questions, and using the wrong one is the usual mistake.

| Question | Read |
| --- | --- |
| What is drawing power right now | `power_watts`, live, through `get device state` |
| Did the load spike this afternoon | `power_watts` history, bucketed: `max` per bucket |
| How much did this use yesterday | `energy_kwh`, bucketed: sum the `delta` pins |
| How much has it used since it was installed | `energy_kwh`, the last raw reading |

For a per-day view, ask for the window you want with one bucket per day and read `delta`. For a month, 30 buckets over 30 days. Because the meter is sampled at a fixed five minutes, buckets of any width are directly comparable, which would not be true if it were sampled on change.

In RUAL Studio this is the **Devices**, then **Home Automation** screen: per-capability history over 1h, 24h, 7d and 30d ranges, plus an energy panel per device. It reads exactly the routes above, and it renders units and chart types from the capability metadata rather than from its own copy. **The in-editor device picker on blocks is a separate piece of that work and is not merged yet** ([admin-frontend PR 1627](https://github.com/rual/admin-frontend/pull/1627)).

### Retention

Readings are kept for 90 days by default and pruned hourly. Hourly rather than daily, because a node that is only up for a few hours a day would otherwise never prune at all.

```
[homeassist.analytics]
# Records nothing when false. The history block then reports recording = false,
# which a flow can tell apart from an empty result.
enabled = true
# Zero disables pruning entirely: only correct where somebody else is watching
# the disk.
retention = "2160h"
flush_interval = "30s"
```

### What This Is Not

- **It is not a metering-grade record.** It is sampled by design, so it answers trends and totals well and forensic questions poorly. "Roughly how much did the boiler use last week" is a question it answers accurately. "What exactly was the reading at 14:32:07" is not.

- **If you need every reading**, for billing or for compliance, write them yourself: a `device state changed` trigger into `create document` gives you an unsampled record in a storage you control, with the cost and the volume that implies. Choose that deliberately, not by accident.

- **Unadopted devices have no history at all**, including for the period before you adopted them. Adoption is when recording starts.

- **History does not survive a store you do not keep.** On a Nano this is the same SQLite file as everything else, so the backup that covers your documents covers this too. Test the restore.

### Next Steps

### Frequently asked

**Does RUAL store every reading a smart home sensor sends?**

No, it stores what changed. Alarms and states are recorded on transitions, measurements when they move beyond a per-capability deadband plus a heartbeat so a stable sensor stays distinguishable from a dead one, meters at a fixed five-minute interval, and button presses individually. A twelve-device house produces roughly 40 reports a minute and about 25 stored rows an hour.

**How do I get energy consumption per day from RUAL?**

Read the energy_kwh capability with get device history and ask for one bucket per day, then use each bucket's delta pin. Energy is a lifetime counter, so the raw value is an ever-rising ramp; the delta is the consumption in that window. A bucket where the counter went backwards, meaning the meter was reset, reports no delta rather than a wrong negative one.

**Why is my RUAL device history chart empty?**

Check the recording pin first. False means this node stores no history for that capability, because analytics is switched off or the capability is not recorded. True with no readings means nothing was stored in that window. Unadopted devices record nothing at all, including for the time before you adopted them.
