Occupancy Data Engine

How the Occupancy Data Engine combines multiple sensors into unified space occupancy state

Overview

The Occupancy Data Engine is Haltian’s backend processing service that transforms raw sensor events from multiple physical devices into unified, accurate occupancy metrics for a space. Rather than treating each sensor independently, it aggregates data from a device group — a virtual device representing a room, zone, or area — and produces a single authoritative occupancy state.

This is in contrast to single-device occupancy, where one sensor (e.g., a desk PIR sensor) directly reports the occupancy of a space it covers alone.

When to Use

ScenarioApproach
Single desk with one sensorSingle device — direct sensor reporting
Small room with one presence sensorSingle device — direct sensor reporting
Large meeting room with entry/exit sensors + motionDevice group
Open office zone with multiple sensorsDevice group
Floor-level occupancy from multiple doorwaysDevice group

Device Groups

Device groups are virtual devices that aggregate measurements from multiple physical sensors. Unlike physical devices, they:

  • Have no hardware component
  • Generate computed measurements from their member devices
  • Use the same UUID structure and API access patterns as physical devices
  • Publish to MQTT with an identical topic structure

A device group is accessible through the deviceGroups query in the Service API. Its member sensors are accessible via the devices relationship, which returns deviceGroupDevices entries — each containing a memberDevice reference to the physical device.

Example Structure

Conference Room A (Device Group)
├── Entryway sensor       — reports both entries and exits
└── Occupancy sensor      — reports presence/absence

A single Entryway sensor tracks movement in both directions, reporting entry and exit counts from one device. A device group can include any number of Entryway sensors and Occupancy sensors — for example, a large meeting room with two doorways would have two Entryway sensors, while an open office zone might combine several Occupancy sensors with Entryway sensors at each entrance. The Occupancy Data Engine aggregates data from all member sensors and produces a single unified occupancy state for the space.

Open Office Zone (Device Group)
├── Entryway sensor (Main door)
├── Entryway sensor (Side door)
├── Occupancy sensor (Area A)
└── Occupancy sensor (Area B)

How the Occupancy Data Engine Works

Processing Pipeline

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#F6FAFA', 'primaryTextColor': '#143633', 'primaryBorderColor': '#143633', 'lineColor': '#143633', 'secondaryColor': '#C7FDE6', 'tertiaryColor': '#73F9C1', 'clusterBkg': '#ffffff', 'clusterBorder': '#143633', 'edgeLabelBackground': '#ffffff'}}}%%
graph TB
    subgraph Input["Sensors"]
        A[Entryway sensor<br/>entries + exits]
        C[Occupancy sensor<br/>presence events]
    end

    subgraph DG["Device Group"]
        D[Virtual Device<br/>aggregation point]
    end

    subgraph ODE["Occupancy Data Engine"]
        E[Event Correlation]
        F[Count Algorithm]
        G[Reset Logic]
    end

    subgraph Output["Output - Occupancy Measurements"]
        H[occupantsCount]
        I[directionalMovement]
        J[occupancyStatus]
    end

    A --> D
    C --> D
    D --> E
    E --> F
    F --> G
    G --> H
    G --> I
    G --> J

Processing Steps

  1. Event ingestion — Raw sensor events (motion detections, entry/exit triggers) arrive from member devices within a device group.
  2. Event correlation — The Occupancy Data Engine correlates events from multiple sensors by time and location to build a coherent picture of movement in the space.
  3. Count algorithm — Entry and exit counts are computed from directional sensors. Motion sensors contribute to occupancy status determination.
  4. Reset logic — The Occupancy Data Engine applies configured reset strategies (scheduled, threshold, timeout) to correct count drift over time.
  5. Output generation — The engine produces standardized occupancy measurements and publishes them under the device group’s identity.

Output Measurements

The Occupancy Data Engine produces three measurement types, all attributed to the device group:

occupants_count

Current number of people in the space.

Schema Type: measurementOccupantsCount

{
  "deviceId": "550e8400-e29b-41d4-a716-446655440000",
  "measuredAt": "2026-02-05T10:30:00.000Z",
  "occupantsCount": 5
}
FieldTypeDescription
measuredAtISO 8601 timestampWhen count was calculated
occupantsCountIntegerNumber of occupants (≥ 0)

directional_movement

Entry and exit tracking.

Schema Type: measurementDirectionalMovement

{
  "deviceId": "550e8400-e29b-41d4-a716-446655440000",
  "measuredAt": "2026-02-05T10:30:00.000Z",
  "entries": 3,
  "exits": 1
}
FieldTypeDescription
entriesIntegerNumber of entries since last reset
exitsIntegerNumber of exits since last reset

occupancy_status

Boolean occupied/vacant status.

Schema Type: measurementOccupancyStatus

{
  "deviceId": "550e8400-e29b-41d4-a716-446655440000",
  "measuredAt": "2026-02-05T10:30:00.000Z",
  "isOccupied": true
}
FieldTypeDescription
isOccupiedBooleantrue if space is occupied, false if vacant

Reset Mechanisms

Occupancy counts can drift over time due to sensor limitations (e.g., two people entering simultaneously being counted as one). The Occupancy Data Engine supports multiple reset strategies to correct this:

TypeDescriptionUse Case
ScheduledReset at specific times (cron expression)Daily midnight reset
ManualAPI-triggered resetOn-demand correction
ThresholdReset when count drops to ≤ NAuto-reset when space empties
TimeoutReset after period of inactivityReset after closing hours

API Access

Occupancy Data Engine measurements are accessed through the same APIs as any device measurement:

  • Service API — Create and manage device groups, and query current and historical occupancy data using the device group’s UUID via GraphQL. Device groups can be managed through Haltian Studio applications or your own software using the Service API.
  • Stream API — Subscribe to real-time occupancy events on the device group’s MQTT topic
  • Data API — Access historical occupancy data via Parquet files

Since device groups use the same UUID structure as physical devices, no special API handling is required — the same queries and subscriptions work for both single devices and device groups.

Querying a Device Group

# Get member devices of a device group
query DeviceGroupDevices($deviceGroupId: uuid) {
  deviceGroupDevices(where: { deviceGroupId: { _eq: $deviceGroupId } }) {
    deviceGroupId
    deviceId
    memberDevice {
      id
      name
      lastSeen
      identifiers {
        tuid
      }
      deviceType {
        make
        model
        productName
      }
    }
  }
}

# Get latest occupancy data for a device group
query MeasurementLastOccupantsCount($device: uuid) {
  measurementLastOccupantsCount(where: { deviceId: { _eq: $device } }) {
    measuredAt
    occupantsCount
  }
}

query MeasurementLastDirectionalMovement($device: uuid) {
  measurementLastDirectionalMovement(where: { deviceId: { _eq: $device } }) {
    measuredAt
    entries
    exits
  }
}

query MeasurementLastOccupancyStatus($device: uuid) {
  measurementLastOccupancyStatus(where: { deviceId: { _eq: $device } }) {
    measuredAt
    isOccupied
  }
}

Note: Pass the device group’s UUID as the $device parameter — device groups use the same UUID structure as physical devices.

MQTT Topics

Occupancy Data Engine measurements are published on the same topic structure as physical devices:

haltian-iot/events/{integration-id}/{api-key-id}/measurements/occupantsCount/{device-group-id}
haltian-iot/events/{integration-id}/{api-key-id}/measurements/directionalMovement/{device-group-id}
haltian-iot/events/{integration-id}/{api-key-id}/measurements/occupancyStatus/{device-group-id}

Next Steps


Troubleshooting

Common issues and solutions when working with the Occupancy Data Engine.