Skip to main content

Overview

The Invernaderos API broadcasts real-time greenhouse sensor data through WebSocket topics using the STOMP protocol. This event-driven architecture ensures all connected clients receive instant updates when new sensor readings arrive.

Available Topics

/topic/greenhouse/messages

Purpose: Real-time greenhouse sensor data broadcasting Message Frequency: Varies based on sensor configuration (typically every 5 seconds) Message Format: RealDataDto JSON object with 22 sensor fields Subscription Example:
stompClient.subscribe('/topic/greenhouse/messages', function(message) {
  const sensorData = JSON.parse(message.body);
  console.log('Received:', sensorData);
});

/topic/greenhouse/statistics

Purpose: Aggregated greenhouse statistics and analytics Message Frequency: On-demand or periodic updates Use Case: Dashboard summaries, trend analysis, alerts Subscription Example:
stompClient.subscribe('/topic/greenhouse/statistics', function(message) {
  const stats = JSON.parse(message.body);
  updateDashboard(stats);
});

Message Format: RealDataDto

All sensor readings are broadcast as RealDataDto objects containing 22 fields:
{
  "timestamp": "2025-03-03T12:30:45.123Z",
  "TEMPERATURA INVERNADERO 01": 24.5,
  "HUMEDAD INVERNADERO 01": 65.2,
  "TEMPERATURA INVERNADERO 02": 23.8,
  "HUMEDAD INVERNADERO 02": 67.1,
  "TEMPERATURA INVERNADERO 03": 25.1,
  "HUMEDAD INVERNADERO 03": 63.5,
  "INVERNADERO_01_SECTOR_01": 1.0,
  "INVERNADERO_01_SECTOR_02": 1.0,
  "INVERNADERO_01_SECTOR_03": 0.0,
  "INVERNADERO_01_SECTOR_04": 1.0,
  "INVERNADERO_02_SECTOR_01": 0.0,
  "INVERNADERO_02_SECTOR_02": 1.0,
  "INVERNADERO_02_SECTOR_03": 1.0,
  "INVERNADERO_02_SECTOR_04": 0.0,
  "INVERNADERO_03_SECTOR_01": 1.0,
  "INVERNADERO_03_SECTOR_02": 0.0,
  "INVERNADERO_03_SECTOR_03": 1.0,
  "INVERNADERO_03_SECTOR_04": 1.0,
  "INVERNADERO_01_EXTRACTOR": 0.0,
  "INVERNADERO_02_EXTRACTOR": 1.0,
  "INVERNADERO_03_EXTRACTOR": 0.0,
  "RESERVA": 0.0,
  "greenhouseId": "550e8400-e29b-41d4-a716-446655440000",
  "tenantId": "SARA"
}

Field Descriptions

Temperature Fields (3 greenhouses):
  • TEMPERATURA INVERNADERO 01/02/03: Temperature readings in Celsius
  • Range: Typically 15-30°C
  • Unit: °C
Humidity Fields (3 greenhouses):
  • HUMEDAD INVERNADERO 01/02/03: Relative humidity percentage
  • Range: 40-80%
  • Unit: %
Field names use SPACES for temperature/humidity (e.g., "TEMPERATURA INVERNADERO 01") to match the physical hardware output format.
12 Sector Fields (4 sectors per greenhouse):
  • INVERNADERO_01_SECTOR_01 through INVERNADERO_03_SECTOR_04
  • Values: Binary (0 = off, 1 = on)
  • Purpose: Irrigation or climate control zones
Naming Convention: Uses UNDERSCORES (e.g., INVERNADERO_01_SECTOR_01)
3 Extractor Fields (1 per greenhouse):
  • INVERNADERO_01_EXTRACTOR, INVERNADERO_02_EXTRACTOR, INVERNADERO_03_EXTRACTOR
  • Values: Binary (0 = off, 1 = on)
  • Purpose: Ventilation fan status
  • timestamp: ISO 8601 timestamp of sensor reading
  • greenhouseId: UUID identifier for the greenhouse
  • tenantId: Tenant identifier for multi-tenant setups
  • RESERVA: Reserved field for future use

Event-Driven Broadcasting

Architecture Flow

The WebSocket broadcasting uses Spring’s event-driven architecture:
1

MQTT Message Reception

Sensor data arrives via MQTT on topic GREENHOUSE/{tenantId}
2

Message Processing

MqttMessageProcessor processes the payload:
  • Parses JSON to RealDataDto
  • Caches in Redis
  • Saves to TimescaleDB
  • Publishes GreenhouseMessageEvent
3

Event Listener

GreenhouseWebSocketHandler listens for events:
@EventListener
fun handleGreenhouseMessage(event: GreenhouseMessageEvent) {
    messagingTemplate.convertAndSend(
        "/topic/greenhouse/messages",
        event.message
    )
}
4

WebSocket Broadcast

All subscribed clients receive the message instantly
Decoupled Design: The MQTT processing and WebSocket broadcasting are decoupled through Spring Events, preventing MQTT message handling from blocking on WebSocket operations.

Subscription Examples

const socket = new SockJS('http://localhost:8080/ws/greenhouse');
const stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
  console.log('Connected:', frame);
  
  // Subscribe to sensor messages
  stompClient.subscribe('/topic/greenhouse/messages', function(message) {
    const data = JSON.parse(message.body);
    
    // Extract specific values
    const temp01 = data['TEMPERATURA INVERNADERO 01'];
    const humidity01 = data['HUMEDAD INVERNADERO 01'];
    const extractor01 = data['INVERNADERO_01_EXTRACTOR'];
    
    console.log(`Greenhouse 01 - Temp: ${temp01}°C, Humidity: ${humidity01}%`);
    console.log(`Extractor: ${extractor01 === 1 ? 'ON' : 'OFF'}`);
    
    // Update UI
    updateTemperatureGauge(temp01);
    updateHumidityGauge(humidity01);
  });
});

Filtering and Processing Messages

Filter by Greenhouse ID

stompClient.subscribe('/topic/greenhouse/messages', function(message) {
  const data = JSON.parse(message.body);
  
  // Only process data for specific greenhouse
  if (data.greenhouseId === 'your-greenhouse-uuid') {
    processSensorData(data);
  }
});

Filter by Tenant ID

const MY_TENANT_ID = 'SARA';

stompClient.subscribe('/topic/greenhouse/messages', function(message) {
  const data = JSON.parse(message.body);
  
  if (data.tenantId === MY_TENANT_ID) {
    updateDashboard(data);
  }
});

Extract Specific Sensors

function extractTemperatures(data) {
  return [
    data['TEMPERATURA INVERNADERO 01'],
    data['TEMPERATURA INVERNADERO 02'],
    data['TEMPERATURA INVERNADERO 03']
  ].filter(temp => temp !== null && temp !== undefined);
}

function extractActiveSectors(data) {
  const sectors = [];
  for (let gh = 1; gh <= 3; gh++) {
    for (let sector = 1; sector <= 4; sector++) {
      const key = `INVERNADERO_0${gh}_SECTOR_0${sector}`;
      if (data[key] === 1) {
        sectors.push({ greenhouse: gh, sector: sector });
      }
    }
  }
  return sectors;
}

Message Handling Best Practices

Parse Safely

try {
  const data = JSON.parse(message.body);
  processData(data);
} catch (error) {
  console.error('Parse error:', error);
}

Handle Missing Fields

const temp = data['TEMPERATURA INVERNADERO 01'] ?? 0;
const humidity = data['HUMEDAD INVERNADERO 01'] ?? 0;

Throttle UI Updates

let lastUpdate = 0;
const updateInterval = 1000; // 1 second

if (Date.now() - lastUpdate > updateInterval) {
  updateUI(data);
  lastUpdate = Date.now();
}

Buffer Data

const dataBuffer = [];
const bufferSize = 100;

dataBuffer.push(data);
if (dataBuffer.length > bufferSize) {
  dataBuffer.shift();
}

Troubleshooting

Possible causes:
  1. Not subscribed to correct topic: /topic/greenhouse/messages
  2. No sensor data being published (check MQTT broker)
  3. Connection dropped (check connected status)
Solution:
// Add connection status monitoring
stompClient.onWebSocketClose = function(event) {
  console.warn('WebSocket closed:', event);
  attemptReconnect();
};
Cause: Unexpected JSON format or encoding issuesSolution:
stompClient.subscribe('/topic/greenhouse/messages', function(message) {
  console.log('Raw message:', message.body); // Debug raw content
  
  try {
    const data = JSON.parse(message.body);
    console.log('Parsed data:', data);
  } catch (error) {
    console.error('Parse failed:', error.message);
    console.error('Message body:', message.body);
  }
});
Cause: Optional sensor fields may be null if sensor is offline or not configuredSolution: Use null coalescing or default values
const temp = data['TEMPERATURA INVERNADERO 01'] ?? null;
if (temp === null) {
  console.warn('Temperature sensor offline');
} else {
  displayTemperature(temp);
}

Next Steps

STOMP Setup

Configure WebSocket connection

REST API

Query historical sensor data

Build docs developers (and LLMs) love