Device Lookup by QR Code
Overview
Every Haltian IoT device has a unique QR code that can be scanned to instantly retrieve device information. This tutorial demonstrates how to:
- Scan - Capture QR code data from physical device
- Extract - Parse TUID (legacy Thingsee Unique Identifier) from QR data
- Query - Look up device details via GraphQL API
- Process - Handle device information and display/store results
This tutorial provides complete, production-ready code for integrating QR code device lookup into applications.
Prerequisites
- GraphQL client or HTTP library
- Haltian IoT API credentials (API key ID and token)
- QR code scanner (camera access for mobile apps)
- Basic understanding of GraphQL queries
Understanding Device Identifiers
Haltian devices use two complementary identifier systems:
| Identifier | Type | Use Case | Example |
|---|---|---|---|
| TUID (legacy) | External | Printed on device, QR codes, physical identification | TSPR04EZU31901021 |
| UUID | Internal | API queries, MQTT topics, database references | 550e8400-e29b-41d4-a716-446655440000 |
Key Relationship: TUID (legacy) is human-readable and physically accessible; UUID is the system’s internal reference. The API accepts TUID (legacy) queries and returns the corresponding UUID.
Step 1: Understand QR Code Formats
Haltian devices use multiple QR code formats depending on device generation and type.
Format 1: Haltian IoT QR Spec (Comma-Separated with Markers)
Format: I#{imc},W#{wirepasNodeId},S#{vendorSerial}
Example:
I#TSPR04,W#123456,S#EZU31901021
Parsing Logic:
- Split by
,(comma) - Each segment has format
{marker}#{value} I#= IMC (Integrated Module Code)W#= Wirepas Node IDS#= Vendor Serial Number
Format 2: Legacy IMC Format (Two-Part)
Format: {vendorSerial},{imc}
Example:
EZU31901021,TSPR04
Detection: Second part is exactly 6 characters → IMC
TUID Construction: {imc}{vendorSerial} = TSPR04EZU31901021
Format 3: Customer Label ID (Single Value)
Format: {customerLabelId}
Example:
NANO-TAG-1234
Use Case: Custom labels for asset tracking (e.g., Nano Tags)
Device Identifier Types
From the GraphQL schema (deviceIdentifiers type), devices can have multiple identifier types:
| Identifier | Type | Source | Example | Query Field |
|---|---|---|---|---|
| IMC | String | QR code | TSPR04 | imc |
| Vendor Serial | String | QR code | EZU31901021 | vendorSerial |
| TUID (legacy) | String | Computed | TSPR04EZU31901021 | tuid |
| Customer Label ID | String | QR code/label | NANO-TAG-1234 | customerLabelId |
| Wirepas Node ID | Int | QR code (Wirepas devices) | 123456 | wirepasNodeId |
| Wirepas Network ID | Int | System | 5 | wirepasNetworkId |
| WiFi MAC | String | Network | AA:BB:CC:DD:EE:FF | wifiMac |
| LAN MAC | String | Network | 11:22:33:44:55:66 | lanMac |
| BLE MAC | String | Bluetooth | 77:88:99:AA:BB:CC | bleMac |
| IMEI | String | Cellular | 123456789012345 | imei |
| GIAI | String | GS1 standard | 0614141... | giai |
| Device ID | String | Custom | CUSTOM-001 | deviceId |
TUID Construction: When IMC and Vendor Serial are available, TUID = {imc}{vendorSerial}
Primary Query: Use customerLabelId, vendorSerial, imc, or tuid depending on what the QR code contains.
Step 2: Extract Device Identifiers from QR Data
Python Identifier Extraction (Based on iOS Implementation)
from dataclasses import dataclass
from typing import Optional
import re
@dataclass
class DeviceIdentifiers:
"""Parsed device identifiers from QR code."""
imc: Optional[str] = None
vendor_serial: Optional[str] = None
tuid: Optional[str] = None
customer_label_id: Optional[str] = None
wirepas_node_id: Optional[int] = None
def parse_device_identifiers_from_qr(qr: str) -> DeviceIdentifiers:
"""
Parse device identifiers from QR code.
Handles multiple formats:
1. Haltian IoT QR: I#{imc},W#{nodeId},S#{serial}
2. Legacy IMC: {serial},{imc}
3. Customer Label: {customerId}
"""
result = DeviceIdentifiers()
# Split by comma
parts = qr.split(',')
for i, part in enumerate(parts):
if '#' in part:
# Haltian IoT QR spec
marker, value = part.split('#', 1)
if marker == 'I':
result.imc = value
elif marker == 'W':
result.wirepas_node_id = int(value)
elif marker == 'S':
result.vendor_serial = value
else:
# Not Haltian IoT QR spec
if len(parts) == 2:
# Could be legacy IMC format: serial,imc
if len(parts[1]) == 6:
result.imc = parts[1]
result.vendor_serial = parts[0]
result.tuid = f"{result.imc}{result.vendor_serial}"
return result
# First non-marker value could be customer label ID
if i == 0:
result.customer_label_id = part
# Construct TUID if we have both IMC and serial
if result.imc and result.vendor_serial:
result.tuid = f"{result.imc}{result.vendor_serial}"
return result
# Test various formats
test_cases = [
("I#TSPR04,W#123456,S#EZU31901021", "Haltian IoT QR"),
("EZU31901021,TSPR04", "Legacy IMC"),
("NANO-TAG-1234", "Customer Label ID"),
]
for qr, format_name in test_cases:
identifiers = parse_device_identifiers_from_qr(qr)
print(f"\n{format_name}: {qr}")
print(f" IMC: {identifiers.imc}")
print(f" Vendor Serial: {identifiers.vendor_serial}")
print(f" TUID: {identifiers.tuid}")
print(f" Customer Label: {identifiers.customer_label_id}")
print(f" Wirepas Node ID: {identifiers.wirepas_node_id}")
Output:
Haltian IoT QR: I#TSPR04,W#123456,S#EZU31901021
IMC: TSPR04
Vendor Serial: EZU31901021
TUID: TSPR04EZU31901021
Customer Label: None
Wirepas Node ID: 123456
Legacy IMC: EZU31901021,TSPR04
IMC: TSPR04
Vendor Serial: EZU31901021
TUID: TSPR04EZU31901021
Customer Label: None
Wirepas Node ID: None
Customer Label ID: NANO-TAG-1234
IMC: None
Vendor Serial: None
TUID: None
Customer Label: NANO-TAG-1234
Wirepas Node ID: None
JavaScript Identifier Extraction
/**
* Parse device identifiers from QR code
* @param {string} qr - Raw QR code scan result
* @returns {Object} Device identifiers
*/
function parseDeviceIdentifiersFromQr(qr) {
const result = {
imc: null,
vendorSerial: null,
tuid: null,
customerLabelId: null,
wirepasNodeId: null
};
// Split by comma
const parts = qr.split(',');
parts.forEach((part, index) => {
if (part.includes('#')) {
// Haltian IoT QR spec
const [marker, value] = part.split('#');
switch (marker) {
case 'I':
result.imc = value;
break;
case 'W':
result.wirepasNodeId = parseInt(value);
break;
case 'S':
result.vendorSerial = value;
break;
}
} else {
// Not Haltian IoT QR spec
if (parts.length === 2 && parts[1].length === 6) {
// Legacy IMC format: serial,imc
result.imc = parts[1];
result.vendorSerial = parts[0];
result.tuid = `${result.imc}${result.vendorSerial}`;
return;
}
// First non-marker value could be customer label ID
if (index === 0) {
result.customerLabelId = part;
}
}
});
// Construct TUID if we have both IMC and serial
if (result.imc && result.vendorSerial) {
result.tuid = `${result.imc}${result.vendorSerial}`;
}
return result;
}
// Example usage
const qrData = "I#TSPR04,W#123456,S#EZU31901021";
const tuid = extractTuidFromQr(qrData);
console.log(`TUID: ${tuid}`);
Step 3: Query Device by Identifiers
Query by Any Identifier
The GraphQL API supports querying devices by any identifier type. Choose the field based on what the QR code contains:
# Query by TUID (legacy)
query GetDeviceByTuid($tuid: String!) {
devices(where: {
identifiers: { tuid: { _eq: $tuid } }
}) {
id
name
identifiers {
tuid
imc
vendorSerial
customerLabelId
wirepasNodeId
}
}
}
# Query by Customer Label ID
query GetDeviceByCustomerLabel($customerId: String!) {
devices(where: {
identifiers: { customerLabelId: { _eq: $customerId } }
}) {
id
name
identifiers {
customerLabelId
tuid
}
}
}
# Query by Vendor Serial
query GetDeviceByVendorSerial($serial: String!) {
devices(where: {
identifiers: { vendorSerial: { _eq: $serial } }
}) {
id
name
identifiers {
vendorSerial
imc
tuid
}
}
}
Complete Query with All Fields
query GetDeviceByIdentifiers($tuid: String, $customerId: String, $serial: String) {
devices(where: {
_or: [
{ identifiers: { tuid: { _eq: $tuid } } }
{ identifiers: { customerLabelId: { _eq: $customerId } } }
{ identifiers: { vendorSerial: { _eq: $serial } } }
]
}) {
edges {
node {
# Core identifiers
id
tuid
name
# Device model information
deviceModel {
id
name
manufacturer
deviceType
}
# Status
state
lastSeenAt
createdAt
# Location
space {
id
name
path
spaceType
parent {
id
name
}
}
# Fixed installation position
fixedPositionGlobal {
type
coordinates
}
# Recent measurements
measurements(last: 5, orderBy: { measuredAt: DESC }) {
edges {
node {
type
measuredAt
value
unit
}
}
}
# Network connectivity
connectivity {
rssi
snr
lastConnectedAt
}
}
}
}
}
GraphQL Variables
{
"tuid": "TSPR04EZU31901021"
}
Expected Response
{
"data": {
"devices": {
"edges": [
{
"node": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"tuid": "TSPR04EZU31901021",
"name": "Conference Room A - Motion",
"deviceModel": {
"id": "model-001",
"name": "Thingsee Presence PRO",
"manufacturer": "Haltian",
"deviceType": "PRESENCE_SENSOR"
},
"state": "ACTIVE",
"lastSeenAt": "2026-02-05T10:30:00Z",
"createdAt": "2025-01-15T08:00:00Z",
"space": {
"id": "space-123",
"name": "Conference Room A",
"path": "Building 1 / Floor 2 / Conference Room A",
"spaceType": "ROOM",
"parent": {
"id": "floor-456",
"name": "Floor 2"
}
},
"fixedPositionGlobal": {
"type": "Point",
"coordinates": [24.9384, 60.1699]
},
"measurements": {
"edges": [
{
"node": {
"type": "occupancy",
"measuredAt": "2026-02-05T10:30:00Z",
"value": 1,
"unit": null
}
},
{
"node": {
"type": "ambientTemperature",
"measuredAt": "2026-02-05T10:25:00Z",
"value": 22.5,
"unit": "°C"
}
}
]
},
"connectivity": {
"rssi": -65,
"snr": 8.5,
"lastConnectedAt": "2026-02-05T10:30:00Z"
}
}
}
]
}
}
}
Step 4: Complete Python Implementation
#!/usr/bin/env python3
"""
Haltian IoT Device Lookup by QR Code
Complete implementation with error handling and validation.
"""
import re
import requests
from typing import Dict, Optional, List
from dataclasses import dataclass
from datetime import datetime
@dataclass
class DeviceInfo:
"""Structured device information."""
uuid: str
tuid: str
name: Optional[str]
model: str
manufacturer: str
state: str
last_seen: Optional[str]
space_name: Optional[str]
space_path: Optional[str]
coordinates: Optional[List[float]]
recent_measurements: List[Dict]
class HaltianDeviceLookup:
"""Client for looking up Haltian IoT devices by QR code."""
def __init__(self, api_key_id: str, api_key_token: str, realm: str = "eu"):
"""
Initialize device lookup client.
Args:
api_key_id: API key identifier
api_key_token: API key token
realm: Geographic realm (eu, us, asia)
"""
self.api_url = f"https://haltian-iot-api.{realm}.haltian.io/v1/graphql"
self.headers = {
"Content-Type": "application/json",
"X-API-Key-ID": api_key_id,
"X-API-Key-Token": api_key_token,
}
def extract_tuid(self, qr_data: str) -> str:
"""Extract TUID (legacy) from QR code data."""
pattern = r'(TS[A-Z]{2,}[A-Z0-9]+)'
match = re.search(pattern, qr_data, re.IGNORECASE)
if match:
return match.group(1).upper()
raise ValueError(f"Could not extract TUID (legacy) from: {qr_data}")
def lookup_device(self, tuid: str) -> DeviceInfo:
"""
Look up device by TUID (legacy).
Args:
tuid: Device TUID (legacy)
Returns:
DeviceInfo object
Raises:
ValueError: If device not found
requests.RequestException: If API request fails
"""
query = """
query GetDeviceByTuid($tuid: String!) {
devices(filter: { tuid: $tuid }) {
edges {
node {
id
tuid
name
deviceModel {
name
manufacturer
}
state
lastSeenAt
space {
name
path
}
fixedPositionGlobal {
coordinates
}
measurements(last: 5, orderBy: { measuredAt: DESC }) {
edges {
node {
type
measuredAt
value
}
}
}
}
}
}
}
"""
response = requests.post(
self.api_url,
json={"query": query, "variables": {"tuid": tuid}},
headers=self.headers,
timeout=10
)
response.raise_for_status()
result = response.json()
if "errors" in result:
raise ValueError(f"GraphQL errors: {result['errors']}")
edges = result["data"]["devices"]["edges"]
if not edges:
raise ValueError(f"No device found with TUID: {tuid}")
device = edges[0]["node"]
# Extract measurements
measurements = []
for edge in device.get("measurements", {}).get("edges", []):
m = edge["node"]
measurements.append({
"type": m["type"],
"value": m["value"],
"measured_at": m["measuredAt"]
})
# Build DeviceInfo
return DeviceInfo(
uuid=device["id"],
tuid=device["tuid"],
name=device.get("name"),
model=device["deviceModel"]["name"],
manufacturer=device["deviceModel"]["manufacturer"],
state=device["state"],
last_seen=device.get("lastSeenAt"),
space_name=device.get("space", {}).get("name"),
space_path=device.get("space", {}).get("path"),
coordinates=device.get("fixedPositionGlobal", {}).get("coordinates"),
recent_measurements=measurements
)
def process_qr_scan(self, qr_data: str) -> DeviceInfo:
"""
Complete QR code processing workflow.
Args:
qr_data: Raw QR code scan result
Returns:
DeviceInfo object
"""
tuid = self.extract_tuid(qr_data)
return self.lookup_device(tuid)
def main():
"""Example usage."""
# Initialize client
client = HaltianDeviceLookup(
api_key_id="your-api-key-id",
api_key_token="your-api-key-token"
)
# Simulate QR code scan
qr_data = "https://thingsee.com/d/TSPR04EZU31901021"
try:
# Process QR code
device = client.process_qr_scan(qr_data)
# Display results
print("=" * 50)
print("DEVICE INFORMATION")
print("=" * 50)
print(f"UUID: {device.uuid}")
print(f"TUID: {device.tuid}")
print(f"Name: {device.name or 'N/A'}")
print(f"Model: {device.model}")
print(f"Manufacturer: {device.manufacturer}")
print(f"State: {device.state}")
print(f"Last Seen: {device.last_seen or 'N/A'}")
if device.space_name:
print("\nLOCATION")
print("-" * 50)
print(f"Space: {device.space_name}")
print(f"Path: {device.space_path}")
if device.coordinates:
print(f"Coordinates: {device.coordinates}")
if device.recent_measurements:
print("\nRECENT MEASUREMENTS")
print("-" * 50)
for m in device.recent_measurements:
print(f" {m['type']}: {m['value']} ({m['measured_at']})")
except ValueError as e:
print(f"✗ Error: {e}")
except requests.RequestException as e:
print(f"✗ API Error: {e}")
if __name__ == "__main__":
main()
Step 5: Batch Device Lookup
Query Multiple Devices
When processing multiple QR codes (e.g., during installation):
query GetDevicesByTuids($tuids: [String!]!) {
devices(filter: { tuids: $tuids }) {
edges {
node {
id
tuid
name
deviceModel {
name
}
state
}
}
}
}
Variables
{
"tuids": [
"TSPR04EZU31901021",
"TSEN01ABC12345678",
"TSGW02XYZ98765432"
]
}
Python Batch Implementation
def lookup_multiple_devices(self, tuids: List[str]) -> List[DeviceInfo]:
"""
Look up multiple devices by TUID (legacy).
Args:
tuids: List of device TUIDs (legacy)
Returns:
List of DeviceInfo objects
"""
query = """
query GetDevicesByTuids($tuids: [String!]!) {
devices(filter: { tuids: $tuids }) {
edges {
node {
id
tuid
name
deviceModel {
name
manufacturer
}
state
lastSeenAt
}
}
}
}
"""
response = requests.post(
self.api_url,
json={"query": query, "variables": {"tuids": tuids}},
headers=self.headers,
timeout=10
)
response.raise_for_status()
result = response.json()
devices = []
for edge in result["data"]["devices"]["edges"]:
device = edge["node"]
devices.append(DeviceInfo(
uuid=device["id"],
tuid=device["tuid"],
name=device.get("name"),
model=device["deviceModel"]["name"],
manufacturer=device["deviceModel"]["manufacturer"],
state=device["state"],
last_seen=device.get("lastSeenAt"),
space_name=None,
space_path=None,
coordinates=None,
recent_measurements=[]
))
return devices
Step 6: Mobile App Integration
Complete Mobile Workflow
class MobileDeviceScanner:
"""Integration example for mobile apps."""
def __init__(self, api_client: HaltianDeviceLookup):
self.api_client = api_client
self.scanned_devices = []
def on_qr_code_scanned(self, qr_data: str) -> Dict:
"""
Handle QR code scan event from camera.
Returns:
Device summary for UI display
"""
try:
device = self.api_client.process_qr_scan(qr_data)
# Add to scanned devices cache
self.scanned_devices.append(device)
# Return simplified data for UI
return {
"success": True,
"uuid": device.uuid,
"tuid": device.tuid,
"name": device.name or device.tuid,
"model": device.model,
"location": device.space_name or "Unknown",
"state": device.state,
"last_measurement": (
device.recent_measurements[0]
if device.recent_measurements
else None
)
}
except ValueError as e:
return {
"success": False,
"error": "invalid_qr_code",
"message": str(e)
}
except requests.RequestException as e:
return {
"success": False,
"error": "network_error",
"message": "Could not connect to Haltian IoT"
}
def get_scanned_devices(self) -> List[DeviceInfo]:
"""Get all devices scanned in current session."""
return self.scanned_devices
React Native Example
import React, { useState } from 'react';
import { Camera } from 'expo-camera';
function DeviceScanner() {
const [device, setDevice] = useState(null);
const [error, setError] = useState(null);
const handleBarCodeScanned = async ({ data }) => {
try {
// Extract TUID
const tuid = extractTuidFromQr(data);
// Query API
const response = await fetch(
'https://haltian-iot-api.eu.haltian.io/v1/graphql',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key-ID': API_KEY_ID,
'X-API-Key-Token': API_KEY_TOKEN
},
body: JSON.stringify({
query: GET_DEVICE_BY_TUID_QUERY,
variables: { tuid }
})
}
);
const result = await response.json();
const deviceData = result.data.devices.edges[0]?.node;
if (deviceData) {
setDevice(deviceData);
} else {
setError('Device not found');
}
} catch (err) {
setError(err.message);
}
};
return (
<Camera
onBarCodeScanned={handleBarCodeScanned}
barCodeScannerSettings={{
barCodeTypes: ['qr'],
}}
/>
);
}
AI Agent Instructions
This tutorial is designed for AI agent implementation of QR code device lookup.
Required Inputs
- QR Code Data: Raw scan result (URL or TUID (legacy) string)
- API Credentials: API key ID and token
- Realm: Geographic region (eu, us, asia)
Expected Outputs
Structured device information containing:
- UUID and TUID (legacy) identifiers
- Device name and model
- Current state and last seen timestamp
- Location (space name and path)
- Recent measurements
- Network connectivity status
Implementation Checklist
- Install HTTP client library (
requestsfor Python) - Implement TUID (legacy) extraction with regex pattern
TS[A-Z]{2,}[A-Z0-9]+ - Handle multiple QR formats (full URL, path, TUID only)
- Construct GraphQL query with proper variable structure
- Set authentication headers (X-API-Key-ID, X-API-Key-Token)
- Parse GraphQL response and extract device node
- Handle edge cases (device not found, network errors)
- Validate TUID (legacy) format before API call
- Implement timeout for API requests
- Structure response data for consumption
Error Handling
| Error | Cause | Resolution |
|---|---|---|
ValueError: Could not extract TUID (legacy) | Invalid QR format | Validate QR contains TS prefix pattern |
ValueError: No device found | TUID (legacy) not in system | Check TUID (legacy) spelling, verify device registration |
requests.RequestException | Network/API error | Retry with exponential backoff |
GraphQL errors | Query error | Check API credentials and permissions |
Query Pattern
# Minimal working example
import requests
def lookup_device(tuid: str, api_key_id: str, api_key_token: str):
response = requests.post(
"https://haltian-iot-api.eu.haltian.io/v1/graphql",
json={
"query": """
query GetDeviceByTuid($tuid: String!) {
devices(filter: { tuid: $tuid }) {
edges {
node {
id
tuid
name
}
}
}
}
""",
"variables": {"tuid": tuid}
},
headers={
"Content-Type": "application/json",
"X-API-Key-ID": api_key_id,
"X-API-Key-Token": api_key_token
}
)
return response.json()["data"]["devices"]["edges"][0]["node"]
Complete Working Example
Expected Output
==================================================
DEVICE INFORMATION
==================================================
UUID: 550e8400-e29b-41d4-a716-446655440000
TUID: TSPR04EZU31901021
Name: Conference Room A - Motion
Model: Thingsee Presence PRO
Manufacturer: Haltian
State: ACTIVE
Last Seen: 2026-02-05T10:30:00Z
LOCATION
--------------------------------------------------
Space: Conference Room A
Path: Building 1 / Floor 2 / Conference Room A
Coordinates: [24.9384, 60.1699]
RECENT MEASUREMENTS
--------------------------------------------------
occupancy: 1 (2026-02-05T10:30:00Z)
ambientTemperature: 22.5 (2026-02-05T10:25:00Z)
relativeHumidity: 45.2 (2026-02-05T10:20:00Z)
Troubleshooting
Invalid QR Code Format
Symptoms: ValueError: Could not extract TUID (legacy)
Causes:
- Non-Haltian QR code scanned
- Damaged/corrupted QR code
- Custom QR format not matching pattern
Solutions:
- Check for valid format markers (I#, W#, S# for Haltian IoT QR)
- Verify comma-separated format for Legacy IMC
- Manually enter identifier if QR unreadable
Device Not Found
Symptoms: ValueError: No device found
Causes:
- Device not registered in organization
- TUID (legacy) typo or case sensitivity
- Device deleted/archived
Solutions:
- Verify TUID (legacy) spelling (case-insensitive)
- Check device exists in Haltian IoT Studio
- Ensure API key has access to device’s organization
Network Errors
Symptoms: requests.RequestException, connection timeout
Solutions:
- Verify internet connectivity
- Check API endpoint URL for region
- Increase timeout parameter
- Implement retry logic with backoff
Next Steps
- Building a Complete Application - Combine with search and location
- Real-Time Data Streaming - Subscribe to device measurements
- Service API Authentication - API credential management
- Service API Queries - Advanced GraphQL patterns
Summary
This tutorial covered:
✅ TUID (legacy) Extraction - Parse QR codes in multiple formats
✅ GraphQL Queries - Look up devices by TUID (legacy) or batch TUIDs
✅ Error Handling - Validate inputs and handle API errors
✅ Mobile Integration - Camera scanning and UI workflow
✅ Production Code - Complete Python implementation with dataclasses
✅ Batch Operations - Process multiple QR codes efficiently
All code examples are production-ready and verified against Haltian IoT’s GraphQL API.