Overview
The Invernaderos API uses RealDataDto as the standard message format for greenhouse sensor data. Each message contains 22 fields representing temperature, humidity, sector irrigation, and extractor status across 3 greenhouses.
The payload structure is defined in RealDataDto.kt with Jackson @JsonProperty annotations for mixed naming conventions.
Message Structure
Complete Schema
{
"timestamp" : "2025-11-16T10:30:00Z" ,
"TEMPERATURA INVERNADERO 01" : 25.5 ,
"HUMEDAD INVERNADERO 01" : 65.2 ,
"TEMPERATURA INVERNADERO 02" : 26.1 ,
"HUMEDAD INVERNADERO 02" : 63.8 ,
"TEMPERATURA INVERNADERO 03" : 24.9 ,
"HUMEDAD INVERNADERO 03" : 67.5 ,
"INVERNADERO_01_SECTOR_01" : 1.0 ,
"INVERNADERO_01_SECTOR_02" : 0.0 ,
"INVERNADERO_01_SECTOR_03" : 1.0 ,
"INVERNADERO_01_SECTOR_04" : 0.0 ,
"INVERNADERO_02_SECTOR_01" : 1.0 ,
"INVERNADERO_02_SECTOR_02" : 1.0 ,
"INVERNADERO_02_SECTOR_03" : 0.0 ,
"INVERNADERO_02_SECTOR_04" : 1.0 ,
"INVERNADERO_03_SECTOR_01" : 0.0 ,
"INVERNADERO_03_SECTOR_02" : 1.0 ,
"INVERNADERO_03_SECTOR_03" : 1.0 ,
"INVERNADERO_03_SECTOR_04" : 0.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 Groups
Temperature & Humidity (6 fields)
Environmental sensors for 3 greenhouses:
Greenhouse 01
Greenhouse 02
Greenhouse 03
Field Type Unit Description TEMPERATURA INVERNADERO 01Double?°C Temperature in greenhouse 1 HUMEDAD INVERNADERO 01Double?% Humidity in greenhouse 1
Typical Range:
Temperature: 15-30°C
Humidity: 40-80%
Example: {
"TEMPERATURA INVERNADERO 01" : 25.5 ,
"HUMEDAD INVERNADERO 01" : 65.2
}
Field Type Unit Description TEMPERATURA INVERNADERO 02Double?°C Temperature in greenhouse 2 HUMEDAD INVERNADERO 02Double?% Humidity in greenhouse 2
Typical Range:
Temperature: 15-30°C
Humidity: 40-80%
Example: {
"TEMPERATURA INVERNADERO 02" : 26.1 ,
"HUMEDAD INVERNADERO 02" : 63.8
}
Field Type Unit Description TEMPERATURA INVERNADERO 03Double?°C Temperature in greenhouse 3 HUMEDAD INVERNADERO 03Double?% Humidity in greenhouse 3
Typical Range:
Temperature: 15-30°C
Humidity: 40-80%
Example: {
"TEMPERATURA INVERNADERO 03" : 24.9 ,
"HUMEDAD INVERNADERO 03" : 67.5
}
Irrigation Sectors (12 fields)
Binary status (0=OFF, 1=ON) for 4 sectors per greenhouse:
Field names use UNDERSCORES (INVERNADERO_01_SECTOR_01), unlike temperature/humidity which use SPACES .
Greenhouse Sectors 01 INVERNADERO_01_SECTOR_01 through INVERNADERO_01_SECTOR_0402 INVERNADERO_02_SECTOR_01 through INVERNADERO_02_SECTOR_0403 INVERNADERO_03_SECTOR_01 through INVERNADERO_03_SECTOR_04
Values:
0.0 or 0 - Sector irrigation OFF
1.0 or 1 - Sector irrigation ON
Example:
{
"INVERNADERO_01_SECTOR_01" : 1.0 , // ON
"INVERNADERO_01_SECTOR_02" : 0.0 , // OFF
"INVERNADERO_01_SECTOR_03" : 1.0 , // ON
"INVERNADERO_01_SECTOR_04" : 0.0 // OFF
}
Ventilation extractor status (0=OFF, 1=ON):
Field Type Description INVERNADERO_01_EXTRACTORDouble?Extractor in greenhouse 1 (0=OFF, 1=ON) INVERNADERO_02_EXTRACTORDouble?Extractor in greenhouse 2 (0=OFF, 1=ON) INVERNADERO_03_EXTRACTORDouble?Extractor in greenhouse 3 (0=OFF, 1=ON)
Example:
{
"INVERNADERO_01_EXTRACTOR" : 0.0 , // OFF
"INVERNADERO_02_EXTRACTOR" : 1.0 , // ON (extracting air)
"INVERNADERO_03_EXTRACTOR" : 0.0 // OFF
}
Field Type Required Description timestampInstant✅ Yes ISO-8601 timestamp (auto-generated if omitted) greenhouseIdString?❌ Optional UUID of greenhouse (auto-assigned by API) tenantIdString?❌ Optional Tenant identifier for multi-tenancy RESERVADouble?❌ Optional Reserved field for future use
Timestamp Format:
"timestamp" : "2025-11-16T10:30:00Z" // ISO-8601 UTC
"timestamp" : "2025-11-16T10:30:00.123Z" // With milliseconds
Data Types & Validation
Type Definitions
From RealDataDto.kt:8-82:
data class RealDataDto (
val timestamp: Instant ,
@JsonProperty ( "TEMPERATURA INVERNADERO 01" ) val temperaturaInvernadero01: Double ? = null ,
@JsonProperty ( "HUMEDAD INVERNADERO 01" ) val humedadInvernadero01: Double ? = null ,
// ... 18 more fields ...
@JsonProperty ( "RESERVA" ) val reserva: Double ? = null ,
val greenhouseId: String ? = null ,
val tenantId: String ? = null
)
Validation Rules
Configured in staging.validation_rules table:
SELECT * FROM staging . validation_rules WHERE sensor_type = 'TEMPERATURE' ;
Rule Value Min Value -50°C Max Value 100°C Typical Range 15-30°C Unit °C (Celsius)
Validation Logic: if ( value < - 50 || value > 100 ) {
throw ValidationException ( "Temperature out of range: $value °C" )
}
SELECT * FROM staging . validation_rules WHERE sensor_type = 'HUMIDITY' ;
Rule Value Min Value 0% Max Value 100% Typical Range 40-80% Unit % (Relative Humidity)
Validation Logic: if ( value < 0 || value > 100 ) {
throw ValidationException ( "Humidity out of range: $value %" )
}
The API uses nullable types (Double?). Missing fields default to null, not 0.0.
Message Processing
API Processing Flow
From MqttMessageProcessor.kt:128-260:
Receive MQTT Message
Message arrives on topic GREENHOUSE/{tenantId} or legacy GREENHOUSE. fun processGreenhouseData (jsonPayload: String , tenantId: String )
Parse JSON to DTO
Convert JSON to RealDataDto using Jackson: val messageDto = jsonPayload. toRealDataDto (
timestamp = Instant. now (),
greenhouseId = tenantId,
tenantId = tenantId
)
Cache in Redis
Store complete message in Redis Sorted Set: greenhouseCacheService. cacheMessage (messageDto)
// Stored in: "greenhouse:messages:{tenantId}"
Transform to SensorReading Entities
Split 22 fields into individual time-series records: data . properties (). forEach { (key, value ) ->
SensorReading (
time = timestamp,
sensorId = key, // "TEMPERATURA INVERNADERO 01"
greenhouseId = greenhouse.id, // UUID
tenantId = tenant.id, // UUID
sensorType = determineSensorType (key), // "TEMPERATURE"
value = value . asDouble (),
unit = determineUnit (key) // "°C"
)
}
Result: 22 JSON fields → 22 database rows
Save to TimescaleDB
Batch insert to iot.sensor_readings hypertable: sensorReadingRepository. saveAll (sensorReadings)
Broadcast via WebSocket
Publish to /topic/greenhouse/messages for real-time clients: eventPublisher. publishEvent ( GreenhouseMessageEvent ( this , messageDto))
Code Examples
Publishing Messages
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
val mapper = jacksonObjectMapper ()
val sensorData = mapOf (
"TEMPERATURA INVERNADERO 01" to 25.5 ,
"HUMEDAD INVERNADERO 01" to 65.2 ,
"INVERNADERO_01_SECTOR_01" to 1.0 ,
"INVERNADERO_01_EXTRACTOR" to 0.0
)
val payload = mapper. writeValueAsString (sensorData)
client. publish ( "GREENHOUSE/SARA" , payload)
Parsing Received Messages
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
val mapper = jacksonObjectMapper ()
client. subscribe ( "GREENHOUSE/+" ) { topic, message ->
val payload = String (message.payload)
val data = mapper. readValue < Map < String , Any >>(payload)
val temp01 = data [ "TEMPERATURA INVERNADERO 01" ] as ? Double
val humidity01 = data [ "HUMEDAD INVERNADERO 01" ] as ? Double
println ( "Temp: $temp01 °C, Humidity: $humidity01 %" )
}
Validating Messages
fun validateSensorData ( data : Map < String , Any ?>): List < String > {
val errors = mutableListOf < String >()
// Validate temperature fields
listOf ( "01" , "02" , "03" ). forEach { num ->
val temp = data [ "TEMPERATURA INVERNADERO $num " ] as ? Double
if (temp != null && (temp < - 50 || temp > 100 )) {
errors. add ( "Temperature $num out of range: $temp °C" )
}
val humidity = data [ "HUMEDAD INVERNADERO $num " ] as ? Double
if (humidity != null && (humidity < 0 || humidity > 100 )) {
errors. add ( "Humidity $num out of range: $humidity %" )
}
}
// Validate sectors
listOf ( "01" , "02" , "03" ). forEach { greenhouse ->
listOf ( "01" , "02" , "03" , "04" ). forEach { sector ->
val value = data [ "INVERNADERO_ ${ greenhouse } _SECTOR_ ${ sector } " ] as ? Double
if ( value != null && value != 0.0 && value != 1.0 ) {
errors. add ( "Sector ${ greenhouse } _ ${ sector } must be 0 or 1: $value " )
}
}
}
return errors
}
Common Patterns
Partial Updates
You can send partial payloads with only changed fields:
{
"TEMPERATURA INVERNADERO 01" : 26.3 ,
"INVERNADERO_01_SECTOR_01" : 1.0
}
API Behavior:
Missing fields are stored as null
Only provided fields are inserted into TimescaleDB
Redis cache stores the complete DTO (with nulls)
Bulk Publishing
For high-frequency sensors, batch multiple readings:
const readings = [
{ "TEMPERATURA INVERNADERO 01" : 25.5 , timestamp: "2025-11-16T10:30:00Z" },
{ "TEMPERATURA INVERNADERO 01" : 25.6 , timestamp: "2025-11-16T10:30:05Z" },
{ "TEMPERATURA INVERNADERO 01" : 25.7 , timestamp: "2025-11-16T10:30:10Z" }
];
readings . forEach ( reading => {
client . publish ( 'GREENHOUSE/SARA' , JSON . stringify ( reading ));
});
The API applies rate limiting to reduce database load. See SensorRateLimiter for throttling logic.
Field Naming Gotcha
CRITICAL: Field names use inconsistent separators :
Temperature/Humidity: SPACES ("TEMPERATURA INVERNADERO 01")
Sectors/Extractors: UNDERSCORES ("INVERNADERO_01_SECTOR_01")
This matches the actual hardware output format. Do NOT change field names or hardware integration will break.
Troubleshooting
Fields Not Parsed (Null Values)
Cause: Incorrect field names (spaces vs underscores)Solution:
Temperature/Humidity: Use SPACES ("TEMPERATURA INVERNADERO 01")
Sectors/Extractors: Use UNDERSCORES ("INVERNADERO_01_SECTOR_01")
Check JSON keys match exactly (case-sensitive)
Cause: Values outside acceptable rangesSolution:
Temperature: -50 to 100°C
Humidity: 0 to 100%
Sectors/Extractors: Only 0 or 1
Check staging.validation_rules table for limits
Cause: Invalid ISO-8601 formatSolution:
Use UTC timezone: 2025-11-16T10:30:00Z
Or omit timestamp (API generates current time)
Avoid non-standard formats: 2025/11/16 10:30:00
Data Not Saved to Database
Cause: Rate limiting or validation failureSolution:
Check API logs for validation errors
Verify SensorRateLimiter allows save frequency
Confirm greenhouse/tenant exists in database
Next Steps
Setup MQTT Client Configure connection to EMQX broker
Authentication Manage MQTT credentials and ACLs