Best Practices
Follow these best practices to build robust, scalable integrations with Thingsee IoT platform.
Data Storage Design
Partition by Device Identity
Structure your data storage around the device identifier (tsmTuid):
-- Example: TimescaleDB hypertable
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
tuid TEXT NOT NULL,
tsm_id INTEGER NOT NULL,
data JSONB
);
SELECT create_hypertable('sensor_data', 'time');
CREATE INDEX idx_tuid ON sensor_data (tuid, time DESC);
Separate Hot and Cold Data
| Data Type | Storage | Retention | Access Pattern |
|---|---|---|---|
| Hot | Time-series DB | 7-30 days | Real-time queries |
| Warm | Column store | 1-2 years | Analytics, reports |
| Cold | Object storage | 7+ years | Compliance, archive |
Thing Identity Handling
Build Identity in Your Cloud
Thingsee devices use serial numbers (tsmTuid) as identifiers. Build meaningful identity mapping in your system:
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#73F9C1','primaryTextColor':'#143633','primaryBorderColor':'#143633','lineColor':'#143633','secondaryColor':'#C7FDE6','tertiaryColor':'#F6FAFA','actorBkg':'#73F9C1','actorBorder':'#143633','noteBorderColor':'#FF8862','noteBkgColor':'#FFCFC0','signalColor':'#143633'}}}%%
sequenceDiagram
participant T as Thingsee Cloud
participant Y as Your Cloud
participant D as Your Database
T->>Y: Message (tsmTuid: TSPR04E2O90201558)
Y->>D: Lookup tsmTuid
D-->>Y: Return: {room: "Meeting Room 1", floor: 3}
Y->>Y: Enrich message with context
Y->>D: Store with business identityRecommended mapping structure:
{
"tsmTuid": "TSPR04E2O90201558",
"location": {
"building": "HQ",
"floor": 3,
"room": "Meeting Room 1",
"zone": "North Wing"
},
"asset": {
"type": "desk",
"id": "DESK-301",
"owner": "Engineering"
},
"metadata": {
"installedDate": "2024-01-15",
"installedBy": "tech@company.com"
}
}
Don’t Use Gateway Identifier for Business Logic
The gateway identifier (tsmGw) is not stable. Sensors may switch between gateways as the mesh network optimizes routing.
Don’t:
// BAD: Using gateway for location
if (message.tsmGw === "TSGW01ABC123") {
location = "Building A";
}
Do:
// GOOD: Using sensor identity for location
const location = await lookupLocation(message.tsmTuid);
Message Handling
Messages Not in Timed Order
Messages may arrive out of chronological order due to:
- Mesh network routing variations
- Gateway buffering during connectivity issues
- Cloud processing pipelines
Always use tsmTs for ordering:
// Sort messages by device timestamp, not arrival time
messages.sort((a, b) => a.tsmTs - b.tsmTs);
// Process in timestamp order
for (const msg of messages) {
await processMessage(msg);
}
Handle Duplicate Messages
Messages may be duplicated in edge cases. Implement idempotent processing:
const messageKey = `${message.tsmTuid}:${message.tsmId}:${message.tsmTs}`;
// Check if already processed
if (await cache.exists(messageKey)) {
logger.debug('Duplicate message, skipping', { messageKey });
return;
}
// Process message
await processMessage(message);
// Mark as processed (with TTL)
await cache.set(messageKey, true, { ttl: 3600 });
Batch Processing
For high-volume deployments, batch database writes:
const BATCH_SIZE = 100;
const BATCH_TIMEOUT = 5000; // ms
class MessageBatcher {
constructor() {
this.buffer = [];
this.timer = null;
}
async add(message) {
this.buffer.push(message);
if (this.buffer.length >= BATCH_SIZE) {
await this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), BATCH_TIMEOUT);
}
}
async flush() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.buffer.length === 0) return;
const batch = this.buffer;
this.buffer = [];
await db.insertMany('sensor_data', batch);
}
}
Error Handling
Graceful Degradation
Design for partial failures:
async function processMessage(message) {
// Primary storage (critical)
try {
await primaryDb.insert(message);
} catch (error) {
logger.error('Primary storage failed', { error });
await deadLetterQueue.add(message);
return; // Don't continue if primary fails
}
// Real-time alerts (best effort)
try {
await alertService.check(message);
} catch (error) {
logger.warn('Alert service unavailable', { error });
// Continue - alerts are not critical
}
// Analytics (async, can retry)
analyticsQueue.add(message).catch(error => {
logger.warn('Analytics queue failed', { error });
});
}
Retry with Backoff
async function retryWithBackoff(fn, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.min(1000 * Math.pow(2, i), 30000);
await sleep(delay);
}
}
}
Deployment Groups
Use Meaningful Group Names
Follow the naming convention:
{prefix}{country}{reserved}{identifier}
Examples:
prfi00haltianhq - Production, Finland, Haltian HQ
prus00siliconvalley - Production, US, Silicon Valley
rdfi00testbench - Development, Finland, Test Bench
Group by Update Strategy
Create groups based on maintenance windows:
| Group | Update Policy | Example |
|---|---|---|
prfi00critical | Manual approval, phased | Production sensors |
prfi00standard | Automatic, scheduled | Non-critical areas |
rdfi00canary | Immediate, test first | Development devices |
Monitoring
Key Metrics to Track
| Metric | Description | Alert Threshold |
|---|---|---|
| Message latency | Time from tsmTs to receipt | > 5 minutes |
| Message rate | Messages per device per hour | < expected rate |
| Battery level | batl property | < 20% |
| Mesh hops | Network diagnostics | > 5 hops |
Health Check Endpoints
app.get('/health', async (req, res) => {
const checks = {
database: await checkDatabase(),
messageQueue: await checkQueue(),
lastMessage: await getLastMessageTime()
};
const healthy = Object.values(checks).every(c => c.ok);
res.status(healthy ? 200 : 503).json(checks);
});
Integration Patterns
Event-Driven Architecture
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#73F9C1','primaryTextColor':'#143633','primaryBorderColor':'#143633','lineColor':'#143633','secondaryColor':'#C7FDE6','tertiaryColor':'#F6FAFA','clusterBkg':'#F6FAFA','clusterBorder':'#143633'}}}%%
flowchart LR
MQTT[MQTT Subscriber] --> Q[Message Queue]
Q --> W1[Worker 1]
Q --> W2[Worker 2]
Q --> W3[Worker 3]
W1 --> DB[(Database)]
W2 --> ALERT[Alert Service]
W3 --> ANALYTICS[Analytics]CQRS for Complex Queries
Separate read and write models:
- Write model - Optimized for message ingestion
- Read model - Optimized for dashboards, reports, queries
Testing
Simulate Device Messages
// Test helper for generating Thingsee messages
function createTestMessage(overrides = {}) {
return {
tsmId: 12100,
tsmEv: 10,
tsmTs: Math.floor(Date.now() / 1000),
tsmTuid: `TEST${Math.random().toString(36).substr(2, 12)}`,
tsmGw: 'TSGW01TEST000001',
temp: 21.5,
humd: 45.0,
...overrides
};
}
Load Testing
Before production, test with expected message volumes plus headroom:
| Devices | Messages/Hour | Test Duration |
|---|---|---|
| 100 | ~600 | 24 hours |
| 1,000 | ~6,000 | 48 hours |
| 10,000 | ~60,000 | 72 hours |