Event-Based Measurements
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:
| Constraint | Event-driven advantage |
|---|---|
| Battery life | Transmitting only on change extends battery life from months to 3–10 years |
| Mesh bandwidth | Fewer messages means the Wirepas mesh can support thousands of devices per gateway |
| Storage efficiency | Storing 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:#143633In 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:
| Trigger | When it fires | Measurement examples |
|---|---|---|
| State change | Value transitions (e.g., occupied → vacant) | occupancyStatus, occupantsCount, directionalMovement |
| Threshold | Value crosses a configured boundary | ambientTemperature, CO2, TVOC |
| Timed | Periodic heartbeat at a configured interval | batteryPercentage, 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
| Measurement | Trigger type | How to interpret gaps |
|---|---|---|
occupancyStatus | State change | Last value is current state. Forward-fill between events. |
occupantsCount | State change | Last count is current. May drift — see ODE resets. |
ambientTemperature | Threshold + timed | Interpolate between readings for higher resolution. |
CO2, TVOC | Threshold + timed | Same as temperature — interpolate for smooth trends. |
batteryPercentage | Timed (infrequent) | Last value is current. Updates are rare (~1% change). |
occupancySeconds | Timed (periodic) | Already aggregated — use directly, no forward-fill needed. |
directionalMovement | State change | Each event is a discrete in/out movement. Count events, don’t forward-fill. |
position, positionZone | State change | Last 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:
- Sort events by timestamp
- Each event’s duration = time until the next event
- 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:
| Type | Processing |
|---|---|
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
- Don’t treat gaps as missing data — A gap means the previous value is still valid
- Forward-fill for aggregation — When computing hourly or daily summaries, carry the last known value forward across period boundaries
- Use time-weighted averages — For numeric measurements like temperature, weight each value by how long it was valid
- Consider period boundaries — The state at the start of any time window is the last event before that window
- Check for pre-aggregated alternatives — Before computing occupancy duration from raw events, check if
occupancySecondsis available
Next Steps
- Data API — Query historical measurement data with code examples
- Occupancy Data Engine — Server-side multi-sensor event aggregation
- Data Model — Complete measurement types reference
- Stream API — Subscribe to events in real time