Best Practices

Design patterns and best practices for Thingsee IoT integrations

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 TypeStorageRetentionAccess Pattern
HotTime-series DB7-30 daysReal-time queries
WarmColumn store1-2 yearsAnalytics, reports
ColdObject storage7+ yearsCompliance, 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 identity

Recommended 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

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:

GroupUpdate PolicyExample
prfi00criticalManual approval, phasedProduction sensors
prfi00standardAutomatic, scheduledNon-critical areas
rdfi00canaryImmediate, test firstDevelopment devices

Monitoring

Key Metrics to Track

MetricDescriptionAlert Threshold
Message latencyTime from tsmTs to receipt> 5 minutes
Message rateMessages per device per hour< expected rate
Battery levelbatl property< 20%
Mesh hopsNetwork 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:

DevicesMessages/HourTest Duration
100~60024 hours
1,000~6,00048 hours
10,000~60,00072 hours