Event-Based Measurements

Why Haltian IoT uses event-driven data instead of periodic sampling, and how to think about measurement timelines

Haltian IoT measurements are event-driven, not periodic time-series samples. This is the single most important concept for anyone consuming sensor data — whether through the Stream API, the Data API, or the Service API.

Why Events, Not Samples?

Traditional IoT platforms sample data at fixed intervals (e.g., every 60 seconds), producing a steady stream of readings whether or not anything changed. Haltian IoT takes a different approach: devices report only when something meaningful happens.

This design is driven by three constraints of mesh-deployed battery sensors:

ConstraintEvent-driven advantage
Battery lifeTransmitting only on change extends battery life from months to 3–10 years
Mesh bandwidthFewer messages means the Wirepas mesh can support thousands of devices per gateway
Storage efficiencyStoring only state changes reduces data volume dramatically

The Value Persistence Rule

The fundamental rule:

A measurement value remains valid until the next event from that device.

If a presence sensor reports occupancyStatus = 1 (occupied) at 10:00, that room is considered occupied until the sensor reports occupancyStatus = 0 (vacant) — whether that’s 5 minutes or 5 hours later.

A gap between events means “no change”, not “no data”.

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F6FAFA', 'primaryTextColor': '#143633', 'primaryBorderColor': '#143633', 'lineColor': '#143633', 'secondaryColor': '#C7FDE6', 'tertiaryColor': '#73F9C1', 'clusterBkg': '#ffffff', 'clusterBorder': '#143633', 'edgeLabelBackground': '#ffffff'}}}%%
flowchart LR
    subgraph Timeline["Measurement Timeline"]
        direction LR
        E1["10:00<br/>status = 1<br/>fa:fa-user Occupied"]
        Gap1["10:01 – 10:47<br/>No events<br/><i>Still occupied</i>"]
        E2["10:47<br/>status = 0<br/>fa:fa-user-slash Vacant"]
        Gap2["10:48 – 11:32<br/>No events<br/><i>Still vacant</i>"]
        E3["11:32<br/>status = 1<br/>fa:fa-user Occupied"]
    end

    E1 --> Gap1 --> E2 --> Gap2 --> E3

    style E1 fill:#73F9C1,stroke:#143633,stroke-width:2px,color:#143633
    style E2 fill:#F6FAFA,stroke:#143633,stroke-width:2px,color:#143633
    style E3 fill:#73F9C1,stroke:#143633,stroke-width:2px,color:#143633
    style Gap1 fill:#C7FDE6,stroke:#143633,stroke-width:1px,color:#143633
    style Gap2 fill:#C7FDE6,stroke:#143633,stroke-width:1px,color:#143633
    style Timeline fill:#ffffff,stroke:#143633,stroke-width:2px,color:#143633

In this example, the database contains 3 records — not the 92 records a 1-minute sample rate would produce for the same period.

What Triggers an Event?

There are three event trigger types, and different measurement types use different triggers:

TriggerWhen it firesMeasurement examples
State changeValue transitions (e.g., occupied → vacant)occupancyStatus, occupantsCount, directionalMovement
ThresholdValue crosses a configured boundaryambientTemperature, CO2, TVOC
TimedPeriodic heartbeat at a configured intervalbatteryPercentage, batteryVoltage, bootCount

Most sensors combine triggers. For example, a temperature sensor may report on threshold crossings and at a configured interval (e.g., every 15 minutes), whichever comes first.

How This Affects Each Measurement Type

MeasurementTrigger typeHow to interpret gaps
occupancyStatusState changeLast value is current state. Forward-fill between events.
occupantsCountState changeLast count is current. May drift — see ODE resets.
ambientTemperatureThreshold + timedInterpolate between readings for higher resolution.
CO2, TVOCThreshold + timedSame as temperature — interpolate for smooth trends.
batteryPercentageTimed (infrequent)Last value is current. Updates are rare (~1% change).
occupancySecondsTimed (periodic)Already aggregated — use directly, no forward-fill needed.
directionalMovementState changeEach event is a discrete in/out movement. Count events, don’t forward-fill.
position, positionZoneState changeLast position is current position.

Calculating Time-in-State

To answer questions like “How long was this room occupied today?”, calculate the duration between consecutive events:

  1. Sort events by timestamp
  2. Each event’s duration = time until the next event
  3. For the last event of the day, decide a boundary (e.g., end of business day, or 1 hour)
Event 1: 08:15 → status = 1 (occupied)     Duration: 2h 30m
Event 2: 10:45 → status = 0 (vacant)        Duration: 1h 15m  
Event 3: 12:00 → status = 1 (occupied)     Duration: until boundary

Total occupied time = sum of durations where status = 1.

For implementation details and code examples, see Working with Event Data in the Data API documentation.

Raw Events vs. Pre-Aggregated Measurements

Some measurements require client-side calculation; others are already aggregated by the platform:

TypeProcessing
Raw events (occupancyStatus, ambientTemperature, etc.)You calculate durations, averages, and aggregations
Pre-aggregated (occupancySeconds)Platform has already computed time-bucketed values — use directly
ODE outputs (occupantsCount from device groups)The Occupancy Data Engine aggregates multiple sensors server-side

Key Principles

  1. Don’t treat gaps as missing data — A gap means the previous value is still valid
  2. Forward-fill for aggregation — When computing hourly or daily summaries, carry the last known value forward across period boundaries
  3. Use time-weighted averages — For numeric measurements like temperature, weight each value by how long it was valid
  4. Consider period boundaries — The state at the start of any time window is the last event before that window
  5. Check for pre-aggregated alternatives — Before computing occupancy duration from raw events, check if occupancySeconds is available

Next Steps