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 Type | Description | Use Case |
|---|---|---|
| Global Position | GPS/GNSS coordinates | Outdoor tracking |
| Local Position | X/Y/Z relative coordinates | Indoor positioning |
| Zone Position | Named zone/area | Room-level tracking |
| RSSI Position | Signal strength based | Proximity detection |
Device Identifiers
When working with position data, understand the device identifiers:
| Identifier | Description | Where Found |
|---|---|---|
| TUID | Thingsee Unique ID | Physical device QR code, GraphQL API |
| UUID | Haltian IoT internal ID | MQTT topics, GraphQL API |
| Device Model | Hardware type identifier | GraphQL API |
Important
MQTT topics use UUID, not TUID. Use the GraphQL API to map between identifiers.
Device Models for Position Tracking
Tags (Tracked Assets)
| Model | Description | Use Case |
|---|---|---|
| Haltian Nano Tag | Small BLE tag with Wirepas | Asset/inventory tracking |
| Haltian Asset Tag | Larger tag with sensors | High-value asset tracking |
Infrastructure (Tracking Infrastructure)
| Model | Description | Use Case |
|---|---|---|
| Haltian EH Locator | Energy harvesting locator | Indoor positioning anchor |
| Haltian IoT Gateway | LAN/4G connected gateway | Data 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"
}
}
| Field | Type | Description |
|---|---|---|
position_local | Object | X/Y/Z coordinates in meters relative to building origin |
position_global | GeoJSON Point | [longitude, latitude] in WGS84 |
accuracy | Number | Estimated accuracy in meters |
floor | Number | Floor number (optional) |
building_id | UUID | Building 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
Note
GeoJSON uses [longitude, latitude] order, which is the opposite of common “lat, long” convention.
Accuracy Considerations
Factors Affecting Accuracy
| Factor | Impact | Mitigation |
|---|---|---|
| Locator density | Low density = lower accuracy | Deploy locators per grid recommendations |
| Obstructions | Walls/metal reduce signal | Increase locator count in obstructed areas |
| Tag battery | Low battery = less frequent updates | Monitor battery levels |
| Movement speed | Fast movement = position lag | Use prediction algorithms |
Accuracy Levels
| Accuracy Range | Typical Use Case |
|---|---|
| < 1m | Desk-level tracking |
| 1-3m | Room-level tracking |
| 3-5m | Zone-level tracking |
| > 5m | Building/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
- Measurement Types - All measurement types reference
- Service API Queries - GraphQL query examples
- Stream API Topics - MQTT topic structure