Eventum Logo

Eventum

IoT Test Data: Synthetic Sensor Telemetry

Generate IoT test data — simulated temperature, humidity, and pressure readings with realistic drift and noise — as a continuous NDJSON stream, no server setup required.

A device fleet does not ship until its readings have been tested against something — a dashboard's thresholds, a time-series pipeline's downsampling, an alert that should fire when a value drifts out of range. A flat random number won't exercise any of it: a real thermometer's next reading starts from wherever the last one left off and moves a small step, not a fresh roll every time. Sensor telemetry is only believable when consecutive readings relate to each other.

The generator below drifts three sensors — temperature, humidity, and pressure — each within a realistic range, and prints every reading as NDJSON to standard output. One eventum generate command runs it — no eventum.yml, startup.yml, or backend to stand up first.

Sensor telemetry test data: drift instead of flat random

A sensor reading is a metric in the sense Logs vs metrics vs events uses the word — a numeric measurement sampled at an instant, without a log's descriptive sentence or a structured event's discrete triggering action. temperature, humidity, and pressure below are exactly that: a name, a value, a unit, a timestamp, and nothing else.

What separates a believable stream of those readings from an obviously generated one is whether consecutive readings relate to each other. Picking each one fresh from a realistic range — module.rand.number.gauss(22.0, 5.0) for temperature, say — keeps every single reading plausible on its own, and still lets two readings five seconds apart land 15 degrees apart, something no real thermometer does. A real sensor's next reading starts from wherever the last one left off and moves a small step from there.

The generator below reads each sensor's last value back out of state before rendering the next one, nudges it by a small step, and clamps the result to the range a real sensor can physically report — the same drift pattern introduced in that lesson for a single CPU-usage metric, applied here across three independent sensors. Sizing that step is itself a distribution choice: Generate realistic fake data names gauss as the fit for exactly this kind of field — a reading that varies around a baseline — so each step below is drawn from a Gaussian centered on zero instead of spread evenly across a fixed range, keeping most ticks small and only rarely letting one run larger, the way real sensor noise actually clusters.

What you'll build

The generator uses:

  • timer input — emits a timestamp every 5 seconds.
  • spin picking mode — cycles through three sensor templates round-robin.
  • locals state — each sensor's last value persists between renders, nudged by a Gaussian step and clamped to a realistic range (see Generate realistic fake data).
  • stdout output with json formatter — validates each event and writes it as NDJSON, one compact JSON object per line.

Prerequisites

Project structure

generator.yml
temperature.jinja
humidity.jinja
pressure.jinja

Build it

Create the project directory

mkdir -p iot-sensors/templates
cd iot-sensors

Write the sensor templates

Each template simulates a different sensor type, using the drift technique above: read the sensor's last value from locals, nudge it with a Gaussian step, clamp the result to a realistic range, and write the new value back before rendering.

Temperature — drifts around 22°C, nudged by a Gaussian step (σ = 0.1) each reading.

templates/temperature.jinja
{% set prev = locals.get("value", 22.0) %}
{% set delta = module.rand.number.gauss(0, 0.1) %}
{% set value = module.rand.number.clamp(prev + delta, 15.0, 35.0) %}
{% do locals.set("value", value) %}
{
  "sensor_id": "sensor-temp-01",
  "metric": "temperature",
  "value": {{ "%.2f" | format(value) }},
  "unit": "celsius",
  "timestamp": "{{ timestamp.isoformat() }}"
}

module.rand.number.clamp(value, 15.0, 35.0) keeps the result between 15°C and 35°C regardless of how a run of same-direction steps would otherwise add up, preventing unrealistic drift over long runs.

Humidity — drifts around 55%, nudged by a Gaussian step (σ = 0.5) each reading.

templates/humidity.jinja
{% set prev = locals.get("value", 55.0) %}
{% set delta = module.rand.number.gauss(0, 0.5) %}
{% set value = module.rand.number.clamp(prev + delta, 20.0, 90.0) %}
{% do locals.set("value", value) %}
{
  "sensor_id": "sensor-hum-01",
  "metric": "humidity",
  "value": {{ "%.1f" | format(value) }},
  "unit": "percent",
  "timestamp": "{{ timestamp.isoformat() }}"
}

Pressure — drifts around 1013 hPa, nudged by a Gaussian step (σ = 0.17) each reading.

templates/pressure.jinja
{% set prev = locals.get("value", 1013.0) %}
{% set delta = module.rand.number.gauss(0, 0.17) %}
{% set value = module.rand.number.clamp(prev + delta, 990.0, 1040.0) %}
{% do locals.set("value", value) %}
{
  "sensor_id": "sensor-pres-01",
  "metric": "pressure",
  "value": {{ "%.1f" | format(value) }},
  "unit": "hPa",
  "timestamp": "{{ timestamp.isoformat() }}"
}

Each sensor template has its own locals — the temperature drift is independent of humidity drift. The value persists between calls, creating smooth, correlated time-series data.

Configure the generator

The timer input emits a timestamp every 5 seconds; with spin mode cycling through three templates, each sensor reports once every 15 seconds — one full cycle through temperature, humidity, and pressure every three ticks.

generator.yml
input:
  - timer:
      seconds: 5
      count: 1

event:
  template:
    mode: spin
    templates:
      - temperature:
          template: templates/temperature.jinja
      - humidity:
          template: templates/humidity.jinja
      - pressure:
          template: templates/pressure.jinja

output:
  - stdout:
      formatter:
        format: json

The json formatter validates each event and writes it on a single line — the NDJSON shape.

Run it

No eventum.yml or startup.yml needed — eventum generate runs a single generator directly:

eventum generate --path generator.yml --id sensors

The readings below were captured by running this generator in sample mode (--live-mode false) with a temporary repeat bound on the timer input, so the run completed immediately instead of pacing to the clock. The command above drops that bound and runs in live mode instead, so readings arrive every 5 seconds in real time rather than all at once.

Readings stream out, cycling through sensors:

{"sensor_id": "sensor-temp-01", "metric": "temperature", "value": 22.15, "unit": "celsius", "timestamp": "2026-07-18T12:38:09.150579+00:00"}
{"sensor_id": "sensor-hum-01", "metric": "humidity", "value": 54.9, "unit": "percent", "timestamp": "2026-07-18T12:38:14.150579+00:00"}
{"sensor_id": "sensor-pres-01", "metric": "pressure", "value": 1012.9, "unit": "hPa", "timestamp": "2026-07-18T12:38:19.150579+00:00"}
{"sensor_id": "sensor-temp-01", "metric": "temperature", "value": 22.08, "unit": "celsius", "timestamp": "2026-07-18T12:38:24.150579+00:00"}
{"sensor_id": "sensor-hum-01", "metric": "humidity", "value": 55.3, "unit": "percent", "timestamp": "2026-07-18T12:38:29.150579+00:00"}

Temperature drifts from 22.15 to 22.08 across two readings fifteen seconds apart; humidity from 54.9 to 55.3 over the same span — small steps in both directions, never a jump. Across the full run, every reading from all three sensors stayed inside its clamp range.

Generate a finite dataset

This generator's timer has no repeat set, so in live mode it never stops on its own — the same unbounded case Streaming vs bulk describes for a cron tick with no end: streaming, by design, for as long as the process keeps running. Turning it into a finite dataset takes the same two things that lesson names for any input — a bound, and sample mode to skip the wait:

generator.yml
input:
  - timer:
      seconds: 5
      count: 1
      repeat: 900
eventum generate --path generator.yml --id sensors-batch --live-mode false > dataset.ndjson

repeat: 900 stops the timer after 900 ticks — 300 full cycles through the three sensors in spin order, 900 readings total — and --live-mode false produces all of them immediately instead of waiting out the 5-second interval between each one.

Pipe to other tools

The NDJSON output works directly with standard Unix tools:

# Filter by sensor type
eventum generate --path generator.yml --id sensors | jq 'select(.metric == "temperature")'

# Write the live stream to a file (Ctrl+C to stop)
eventum generate --path generator.yml --id sensors > readings.ndjson

# Count readings per sensor so far (run for a while, then Ctrl+C)
eventum generate --path generator.yml --id sensors | jq -r '.sensor_id' | sort | uniq -c

Going further

  • Anomaly injection — use module.rand.chance(0.02) to occasionally spike a value far outside its normal range, simulating a sensor fault.
  • Multiple sensor sites — run this generator several times with a different --id and a distinct site value baked into each template, simulating a fleet of installations instead of one.
  • A real backend instead of standard output — swap the stdout output for kafka, opensearch, or any other output plugin; see Stream synthetic data to your stack for wiring a generator directly to the systems you run.
  • Historical dataset — replace timer with linspacestart/end spanning a year, count set so consecutive timestamps land five minutes apart — to generate a full year of readings in sample mode.
  • Alert thresholds — swap locals for globals state, visible across every generator in the process, so a second generator can watch each sensor's latest value and produce alerts when it crosses a threshold.

What's next

FAQ

  • The Logs vs metrics vs events lesson for the metric shape this generator produces, and the single-sensor drift example this tutorial builds into a full three-sensor generator
  • The Generate realistic fake data lesson for Gaussian and other skewed distributions, and clamping a value to a realistic range
  • The Streaming vs bulk lesson for live mode's continuous feed versus a bounded sample-mode batch, the general rule behind this generator's finite-dataset step
  • The Stream synthetic data to your stack pillar for delivering these readings to a real backend instead of standard output
  • The NDJSON format lesson for the newline-delimited JSON shape this generator writes
  • The Scenarios track for the wider picture of applied synthetic data
  • Ready-made generators in the Eventum Hub

On this page