Position Data Best Practices

Best practices for working with Haltian IoT position and location data

This guide provides best practices for working with position data from Haltian IoT devices, including asset tags, locators, and presence sensors.

Overview

Haltian IoT provides several types of position data:

Data TypeDescriptionUse Case
Global PositionGPS/GNSS coordinatesOutdoor tracking
Local PositionX/Y/Z relative coordinatesIndoor positioning
Zone PositionNamed zone/areaRoom-level tracking
RSSI PositionSignal strength basedProximity detection

Device Identifiers

When working with position data, understand the device identifiers:

IdentifierDescriptionWhere Found
TUIDThingsee Unique IDPhysical device QR code, GraphQL API
UUIDHaltian IoT internal IDMQTT topics, GraphQL API
Device ModelHardware type identifierGraphQL API

Device Models for Position Tracking

Tags (Tracked Assets)

ModelDescriptionUse Case
Haltian Nano TagSmall BLE tag with WirepasAsset/inventory tracking
Haltian Asset TagLarger tag with sensorsHigh-value asset tracking

Infrastructure (Tracking Infrastructure)

ModelDescriptionUse Case
Haltian EH LocatorEnergy harvesting locatorIndoor positioning anchor
Haltian IoT GatewayLAN/4G connected gatewayData backhaul

Position Measurement Types

PositionMeasurement

The primary position measurement containing location data:

{
  "measured_at": "2025-01-28T10:30:00.000Z",
  "value": {
    "position_local": {
      "x": 12.5,
      "y": 8.3,
      "z": 0.0
    },
    "position_global": {
      "type": "Point",
      "coordinates": [24.9384, 60.1699]
    },
    "accuracy": 2.5,
    "floor": 2,
    "building_id": "building-uuid"
  }
}
FieldTypeDescription
position_localObjectX/Y/Z coordinates in meters relative to building origin
position_globalGeoJSON Point[longitude, latitude] in WGS84
accuracyNumberEstimated accuracy in meters
floorNumberFloor number (optional)
building_idUUIDBuilding identifier (optional)

PositionMeasurementZone

Zone-based position indicating which named area the device is in:

{
  "measured_at": "2025-01-28T10:30:00.000Z",
  "value": {
    "zone_id": "zone-uuid",
    "zone_name": "Conference Room A",
    "zone_type": "meeting_room",
    "entered_at": "2025-01-28T10:25:00.000Z"
  }
}

Querying Position Data

Latest Position via GraphQL

query GetDevicePosition($deviceId: ID!) {
  device(id: $deviceId) {
    id
    tuid
    name
    lastPosition: measurements(
      filter: { types: ["position"] }
      last: 1
    ) {
      edges {
        node {
          measuredAt
          value
        }
      }
    }
    lastZone: measurements(
      filter: { types: ["position_zone"] }
      last: 1
    ) {
      edges {
        node {
          measuredAt
          value
        }
      }
    }
  }
}

Position History

query GetPositionHistory($deviceId: ID!, $from: DateTime!, $to: DateTime!) {
  device(id: $deviceId) {
    measurements(
      filter: {
        types: ["position"]
        from: $from
        to: $to
      }
      first: 1000
    ) {
      edges {
        node {
          measuredAt
          value
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

Find Devices in Zone

query GetDevicesInZone($zoneId: ID!) {
  space(id: $zoneId) {
    name
    devices(filter: { state: ACTIVE }) {
      edges {
        node {
          id
          tuid
          name
          lastSeenAt
        }
      }
    }
  }
}

MQTT Position Streams

Subscribe to real-time position updates:

# Position measurements
"haltian-iot/events/{integration}/{apikey}/measurements/position/#"

# Zone changes
"haltian-iot/events/{integration}/{apikey}/measurements/position_zone/#"

Python Example: Track Asset Positions

#!/usr/bin/env python3
"""
Track asset positions in real-time via MQTT.
"""

import ssl
import json
from datetime import datetime
import paho.mqtt.client as mqtt

# Track latest positions
positions = {}

def on_message(client, userdata, msg):
    topic_parts = msg.topic.split("/")
    measurement_type = topic_parts[5]
    device_id = topic_parts[6]
    
    payload = json.loads(msg.payload.decode("utf-8"))
    
    if measurement_type == "position":
        value = payload["value"]
        positions[device_id] = {
            "timestamp": payload["measured_at"],
            "local": value.get("position_local"),
            "global": value.get("position_global"),
            "accuracy": value.get("accuracy")
        }
        
        # Log position update
        local = value.get("position_local", {})
        print(f"[{device_id[:8]}...] Position: "
              f"({local.get('x', 'N/A')}, {local.get('y', 'N/A')}) "
              f"accuracy: {value.get('accuracy', 'N/A')}m")
    
    elif measurement_type == "position_zone":
        value = payload["value"]
        print(f"[{device_id[:8]}...] Entered zone: {value.get('zone_name')}")

Coordinate Systems

Local Coordinates

Local coordinates use a Cartesian system relative to a building origin:

  • X: East-West axis (positive = East)
  • Y: North-South axis (positive = North)
  • Z: Vertical axis (positive = Up)
  • Units: Meters

Global Coordinates

Global coordinates use WGS84 (standard GPS coordinate system):

  • Format: GeoJSON Point [longitude, latitude]
  • Longitude: -180 to +180
  • Latitude: -90 to +90

Accuracy Considerations

Factors Affecting Accuracy

FactorImpactMitigation
Locator densityLow density = lower accuracyDeploy locators per grid recommendations
ObstructionsWalls/metal reduce signalIncrease locator count in obstructed areas
Tag batteryLow battery = less frequent updatesMonitor battery levels
Movement speedFast movement = position lagUse prediction algorithms

Accuracy Levels

Accuracy RangeTypical Use Case
< 1mDesk-level tracking
1-3mRoom-level tracking
3-5mZone-level tracking
> 5mBuilding/floor level

Best Practices

1. Use Appropriate Position Type

# For room-level tracking, use zone positions
if use_case == "meeting_room_occupancy":
    subscribe_to("measurements/position_zone/#")

# For asset tracking, use precise positions
if use_case == "asset_tracking":
    subscribe_to("measurements/position/#")

2. Handle Missing Data

def get_position(measurement):
    value = measurement.get("value", {})
    
    # Prefer local coordinates for indoor
    local = value.get("position_local")
    if local:
        return {"type": "local", "x": local["x"], "y": local["y"]}
    
    # Fall back to global
    global_pos = value.get("position_global")
    if global_pos:
        coords = global_pos["coordinates"]
        return {"type": "global", "lng": coords[0], "lat": coords[1]}
    
    # Fall back to zone
    zone = value.get("zone_name")
    if zone:
        return {"type": "zone", "name": zone}
    
    return None

3. Filter by Accuracy

def is_position_reliable(measurement, max_accuracy=5.0):
    """Check if position accuracy is acceptable."""
    accuracy = measurement.get("value", {}).get("accuracy")
    return accuracy is not None and accuracy <= max_accuracy

4. Aggregate for Heat Maps

from collections import defaultdict

zone_visits = defaultdict(int)

def on_zone_message(device_id, zone_name):
    zone_visits[zone_name] += 1

# Generate heat map data
def get_heat_map():
    total = sum(zone_visits.values())
    return {
        zone: count / total 
        for zone, count in zone_visits.items()
    }

5. Handle Stale Data

from datetime import datetime, timedelta

STALE_THRESHOLD = timedelta(minutes=5)

def is_position_current(measurement):
    """Check if position data is recent."""
    measured_at = datetime.fromisoformat(
        measurement["measured_at"].replace("Z", "+00:00")
    )
    age = datetime.now(measured_at.tzinfo) - measured_at
    return age < STALE_THRESHOLD

Next Steps