Meter Readings
The ERP Toolkit provides specialized support for meter reading data, a common use case in utility and energy industry integrations.
Overview​
Meter readings require special handling because they:
- Come as arrays of readings per meter
- Need to be linked to existing meter entities
- Contain time-series data with timestamps and values
Configuration Structure​
Meter readings are configured separately from entity mappings:
{
"entities": [...],
"meter_readings": [
{
"jsonataExpression": "$.readings",
"meter": {
"unique_ids": [
{ "attribute": "external_id", "field": "meter_id" }
]
},
"fields": [
{ "attribute": "external_id", "field": "reading_id" },
{ "attribute": "timestamp", "field": "read_at" },
{ "attribute": "source", "constant": "ERP" },
{ "attribute": "value", "field": "reading_value" }
]
}
]
}
Required Fields​
Every meter reading configuration must include mappings for these fields:
| Field | Description |
|---|---|
external_id | Unique identifier for this reading |
timestamp | When the reading was taken (ISO 8601 format) |
source | Origin of the reading (e.g., "ERP", "MANUAL") |
value | The actual meter reading value |
Missing required fields will result in a validation error:
Meter readings configuration (item 0) is missing required field attributes: timestamp, source
Meter Configuration​
Meter Lookup​
The meter object defines how to find the associated meter entity:
{
"meter": {
"unique_ids": ["meter_number"]
}
}
| Property | Type | Description |
|---|---|---|
unique_ids | string[] | Fields to identify the meter entity |
Meter Identifier Source​
The meter identifier value comes from the reading data:
// Input payload
{
"readings": [
{
"meter_number": "M001",
"readingDate": "2024-01-15",
"readingValue": 12500
}
]
}
The meter_number field within each reading is used to find the meter.
Meter Counter (Multi-Tariff)​
For meters with multiple counters (e.g., day/night tariffs), specify the counter:
{
"meter": {
"unique_ids": [
{ "attribute": "external_id", "field": "meter_id" }
]
},
"meter_counter": {
"unique_ids": [
{ "attribute": "external_id", "field": "counter_id" }
]
},
"fields": [...]
}
Field Mappings​
Basic Reading Fields​
{
"fields": [
{ "attribute": "reading_date", "field": "date" },
{ "attribute": "value", "field": "reading" },
{ "attribute": "unit", "constant": "kWh" }
]
}
Reading Types​
Map the reading type or category:
{
"fields": [
{ "attribute": "reading_date", "field": "date" },
{ "attribute": "value", "field": "reading" },
{ "attribute": "reading_type", "field": "type" },
{ "attribute": "source", "constant": "ERP_IMPORT" }
]
}
Multiple Value Types​
Some meters have multiple reading values (e.g., HT/NT for electricity):
{
"meter_readings": [
{
"jsonataExpression": "$.readings.{'meter_number': meterId, 'reading_date': date, 'value': htValue, 'tariff': 'HT'}",
"meter": { "unique_ids": ["meter_number"] },
"fields": [
{ "attribute": "reading_date", "field": "reading_date" },
{ "attribute": "value", "field": "value" },
{ "attribute": "tariff", "field": "tariff" }
]
},
{
"jsonataExpression": "$.readings.{'meter_number': meterId, 'reading_date': date, 'value': ntValue, 'tariff': 'NT'}",
"meter": { "unique_ids": ["meter_number"] },
"fields": [
{ "attribute": "reading_date", "field": "reading_date" },
{ "attribute": "value", "field": "value" },
{ "attribute": "tariff", "field": "tariff" }
]
}
]
}
JSONata Expressions​
Extract Readings Array​
{
"jsonataExpression": "$.readings"
}
From payload:
{
"readings": [
{ "meterId": "M001", "date": "2024-01-15", "value": 12500 },
{ "meterId": "M002", "date": "2024-01-15", "value": 8300 }
]
}
Flatten Nested Structures​
{
"jsonataExpression": "$.meters.readings"
}
From payload:
{
"meters": [
{
"readings": [
{ "meter_number": "M001", "date": "2024-01-15", "value": 12500 }
]
}
]
}
Transform Before Mapping​
{
"jsonataExpression": "$.readings.{'meter_number': meterId, 'reading_date': $fromMillis(timestamp), 'value': $number(readingValue)}"
}
Complete Example​
Input Payload​
{
"batchId": "BATCH-2024-01-15",
"readingDate": "2024-01-15T08:00:00Z",
"readings": [
{
"meterId": "M001",
"meterType": "electricity",
"currentReading": 12500.5,
"previousReading": 12000.0,
"unit": "kWh"
},
{
"meterId": "M002",
"meterType": "gas",
"currentReading": 450.25,
"previousReading": 420.00,
"unit": "m3"
}
]
}
Mapping Configuration​
{
"meter_readings": [
{
"jsonataExpression": "readings",
"meter": {
"unique_ids": ["meter_number"]
},
"fields": [
{ "attribute": "meter_number", "field": "meterId" },
{ "attribute": "reading_date", "field": "$$.readingDate" },
{ "attribute": "value", "field": "currentReading" },
{ "attribute": "previous_value", "field": "previousReading" },
{ "attribute": "unit", "field": "unit" },
{
"attribute": "consumption",
"jsonataExpression": "currentReading - previousReading"
},
{ "attribute": "source", "constant": "ERP_BATCH_IMPORT" },
{ "attribute": "batch_id", "field": "$$.batchId" }
]
}
]
}
Note: $$ references the root payload, allowing access to batch-level fields from within the readings array.
Deduplication​
Meter readings are deduplicated based on:
- Meter identifier
- Reading date
- Reading type (if specified)
Duplicate readings within a 24-hour window are skipped.
Processing Flow​
Extract Readings Array (JSONata)
│
â–¼
For Each Reading:
│
├──▶ Extract Meter Identifier
│
├──▶ Find Meter Entity
│ │
│ ├─ Found ──▶ Continue
│ │
│ └─ Not Found ──▶ Skip/Error
│
├──▶ Apply Field Mappings
│
├──▶ Check Deduplication
│ │
│ ├─ Unique ──▶ Create Reading
│ │
│ └─ Duplicate ──▶ Skip
│
└──▶ Create meter_reading Entity
Error Handling​
Meter Not Found​
{
"status": "error",
"message": "Meter not found",
"error": {
"code": "METER_NOT_FOUND",
"details": {
"meter_number": "M999"
}
}
}
Resolution: Ensure meters exist before importing readings, or process meters first.
Invalid Reading Value​
{
"status": "error",
"message": "Invalid reading value",
"error": {
"code": "INVALID_READING_VALUE",
"details": {
"field": "value",
"received": "not-a-number"
}
}
}
Resolution: Validate and transform values using JSONata before mapping.
Reading Matching Strategies​
By default, meter readings use external_id for upsert matching. However, when readings originate from the End Customer Portal (ECP) and are later echoed back by the ERP, duplicates can occur because ECP readings may not have an external_id initially.
The reading_matching option configures how incoming readings are matched:
| Strategy | Description |
|---|---|
external_id | Default. Match readings by external_id attribute |
strict-date | Match by meter_id + counter_id + direction + date (German timezone) |
Using strict-date​
{
"meter_readings": [
{
"reading_matching": "strict-date",
"meter": { ... },
"fields": [ ... ]
}
]
}
How strict-date works:
- Before creating a reading, the system looks up existing readings for the same meter_id + counter_id + direction on the same German calendar day
- If a single match is found: Updates the existing reading with ERP data
- If multiple matches are found: Logs an error and skips (to avoid duplicates)
- If no match is found: Creates a new reading
This strategy is useful for ECP-to-ERP roundtrip scenarios where the ERP echoes readings back with truncated timestamps.
Best Practices​
Ensure Meters Exist First​
Process meters before readings:
{
"entities": [
{
"entity_schema": "meter",
"unique_ids": ["meter_number"],
"fields": [...]
}
],
"meter_readings": [...]
}
Use Batch Identifiers​
Include batch information for traceability:
{
"fields": [
{ "attribute": "batch_id", "field": "$$.batchId" },
{ "attribute": "import_timestamp", "field": "$$.importTime" }
]
}
Handle Missing Readings Gracefully​
Use conditional processing:
{
"jsonataExpression": "readings[value != null and value >= 0]"
}
Validate Value Ranges​
Use JSONata to validate readings:
{
"attribute": "value",
"jsonataExpression": "currentReading >= 0 ? currentReading : $error('Negative reading value')"
}
Next Steps​
- Examples - Complete integration examples with meter readings