Elvaco LoRa Payload Parser
This is a parser provided by Elvaco capable of decoding all message formats supported by existing Elvaco LoRa devices into a common JSON format. The parser is written to work with The Things Network, LORIOT, Chirpstack and alike.
The parser in this service is for testing purposes only. We do not guarantee that it will always produce correct results. Use it at your own risk, and we disclaim any liability for any damages or losses.
The expected input should be a payload encoded in hexadecimal format.
Decoder source
// CMi41xx
const packetTypes = {
// CMi4110
0x00: ["CMi4110", "Standard", "Landis+Gyr"],
0x01: ["CMi4110", "Compact", "Landis+Gyr"],
0x02: ["CMi4110", "JSON", "Landis+Gyr"],
0x03: ["CMi4110", "Scheduled Daily Redundant", "Landis+Gyr", ["", "", "", "accAt24", ""]],
0x04: ["CMi4110", "Scheduled Extended", "Landis+Gyr"],
0x3F: ["CMi4110", "Scheduled Extended+", "Landis+Gyr", ["", "tariff1", "tariff2", "tariff3", "", ""]],
0x40: ["CMi4110", "Scheduled Extended+ 2", "Landis+Gyr"],
0x41: ["CMi4110", "Compact Tariff", "Landis+Gyr", ["", "tariff1", "tariff2", "tariff3", "", ""]],
0x46: ["CMi4110", "Maximum Flow", "Landis+Gyr"],
0x47: ["CMi4110", "Scheduled Daily Redundant Tariff", "Landis+Gyr", ["at24", "", "", "", "", ""]],
0x48: ["CMi4110", "Scheduled Daily Redundant Tariff 2", "Landis+Gyr"],
0x49: ["CMi4110", "Scheduled Monthly", "Landis+Gyr"],
0x4A: ["CMi4110", "Scheduled Daily", "Landis+Gyr", ["at24", "", "", "", "", ""]],
// CMi4111
0x05: ["CMi4111", "Standard", "Landis+Gyr"],
0x06: ["CMi4111", "Compact", "Landis+Gyr"],
0x07: ["CMi4111", "JSON", "Landis+Gyr"],
0x08: ["CMi4111", "Scheduled - Daily redundant", "Landis+Gyr", ["", "", "", "", "", "accAt24"]],
0x09: ["CMi4111", "Scheduled - Extended", "Landis+Gyr"],
0x0A: ["CMi4111", "Combined heat/cooling", "Landis+Gyr"],
0x0B: ["CMi4111", "Simple billing", "Landis+Gyr"],
0x0C: ["CMi4111", "Plausibility check", "Landis+Gyr"],
0x0D: ["CMi4111", "Monitoring", "Landis+Gyr"],
// CMi4130
0x0F: ["CMi4130", "Standard", "Itron"],
0x10: ["CMi4130", "Compact", "Itron"],
0x11: ["CMi4130", "JSON", "Itron"],
0x12: ["CMi4130", "Scheduled - Daily redundant", "Itron", ["", "", "", "", "", "accAt24"]],
0x13: ["CMi4130", "Scheduled - Extended", "Itron"],
0x14: ["CMi4130", "Combined heat/cooling", "Itron"],
// CMi4140
0x15: ["CMi4140", "Standard", "Kamstrup"],
0x16: ["CMi4140", "Compact", "Kamstrup"],
0x17: ["CMi4140", "JSON", "Kamstrup"],
0x18: ["CMi4140", "Scheduled Daily Redundant", "Kamstrup"],
0x19: ["CMi4140", "Scheduled Extended", "Kamstrup"],
0x1A: ["CMi4140", "Combined heating/cooling", "Kamstrup"],
0x1B: ["CMi4140", "Heat Intelligence", "Kamstrup", ["E1", "E3", "", "", "", ""]],
0x3B: ["CMi4140", "Scheduled Extended+", "Kamstrup"],
0x3C: ["CMi4140", "Scheduled Extended+ 2", "Kamstrup"],
0x1C: ["CMi4140", "Pulse", "Kamstrup"],
0x1D: ["CMi4140", "Pulse 2", "Kamstrup"],
0x4D: ["CMi4140", "Pulse Extended", "Kamstrup"],
0x4E: ["CMi4140", "Pulse Extended 2", "Kamstrup"],
0x4F: ["CMi4140", "DR0 message", "Kamstrup"],
// CMi4160
0x1E: ["CMi4160", "Standard", "Diehl"],
0x1F: ["CMi4160", "Compact", "Diehl"],
0x20: ["CMi4160", "JSON", "Diehl"],
0x21: ["CMi4160", "Scheduled - Daily redundant", "Diehl"],
0x22: ["CMi4160", "Scheduled - Extended", "Diehl"],
0x23: ["CMi4160", "Combined heat/cooling", "Diehl"],
0x3D: ["CMi4160", "Scheduled Extended+", "Diehl"],
0x3E: ["CMi4160", "Scheduled Extended+ 2", "Diehl"],
// CMi4170
0x24: ["CMi4170", "Standard", "Engelmann"],
0x25: ["CMi4170", "Compact", "Engelmann"],
0x26: ["CMi4170", "JSON", "Engelmann"],
0x27: ["CMi4170", "Scheduled - daily redundant", "Engelmann"],
0x28: ["CMi4170", "Scheduled - Extended", "Engelmann"],
0x29: ["CMi4170", "Combined heat/cooling", "Engelmann"],
0x2C: ["CMi4170", "Engelmann", "Engelmann"],
0x2D: ["CMi4170", "Engelmann 2", "Engelmann"],
0x50: ["CMi4170", "DR0 message", "Engelmann"],
};
const valueMap = {
// energy
0x0400: ["energy", "Wh", 0.001],
0x0401: ["energy", "Wh", 0.01],
0x0402: ["energy", "Wh", 0.1],
0x0403: ["energy", "Wh", 1],
0x0404: ["energy", "Wh", 10],
0x0405: ["energy", "Wh", 100],
0x0406: ["energy", "kWh", 1],
0x0407: ["energy", "kWh", 10],
0x0408: ["energy", "J", 1],
0x0409: ["energy", "J", 10],
0x040a: ["energy", "J", 100],
0x040b: ["energy", "J", 1000],
0x040c: ["energy", "J", 10000],
0x040d: ["energy", "J", 100000],
0x040e: ["energy", "MJ", 1],
0x040f: ["energy", "MJ", 10],
0x04fb: {
0x0d: ["energy", "MCal", 1],
0x0e: ["energy", "MCal", 10],
0x0f: ["energy", "MCal", 100],
// cooling energy
0x8d: ["coolingEnergy", "MCal", 1],
0x8e: ["coolingEnergy", "MCal", 10],
0x8f: ["coolingEnergy", "MCal", 100],
},
0x4400: ["accumulatedEnergy", "Wh", 0.001],
0x4401: ["accumulatedEnergy", "Wh", 0.01],
0x4402: ["accumulatedEnergy", "Wh", 0.1],
0x4403: ["accumulatedEnergy", "Wh", 1],
0x4404: ["accumulatedEnergy", "Wh", 10],
0x4405: ["accumulatedEnergy", "Wh", 100],
0x4406: ["accumulatedEnergy", "kWh", 1],
0x4407: ["accumulatedEnergy", "kWh", 10],
0x440e: ["accumulatedEnergy", "MJ", 1],
0x440f: ["accumulatedEnergy", "MJ", 10],
0x44fb: {
0x0d: ["accumulatedEnergy", "MCal", 1],
0x0e: ["accumulatedEnergy", "MCal", 10],
0x0f: ["accumulatedEnergy", "MCal", 100],
},
// cooling energy, have additional 0xff02 flag
0x0480: ["coolingEnergy", "Wh", 0.001],
0x0481: ["coolingEnergy", "Wh", 0.01],
0x0482: ["coolingEnergy", "Wh", 0.1],
0x0483: ["coolingEnergy", "Wh", 1],
0x0484: ["coolingEnergy", "Wh", 10],
0x0485: ["coolingEnergy", "Wh", 100],
0x0486: ["coolingEnergy", "kWh", 1],
0x0487: ["coolingEnergy", "kWh", 10],
0x048e: ["coolingEnergy", "MJ", 1],
0x048f: ["coolingEnergy", "MJ", 10],
// energy E8, E9
0x04ff: {
0x07: ["energyE8", "m³*°C", 1],
0x08: ["energyE9", "m³*°C", 1],
},
// volume
0x0410: ["volume", "m³", 0.000001],
0x0411: ["volume", "m³", 0.00001],
0x0412: ["volume", "m³", 0.0001],
0x0413: ["volume", "m³", 0.001],
0x0414: ["volume", "m³", 0.01],
0x0415: ["volume", "m³", 0.1],
0x0416: ["volume", "m³", 1],
0x0417: ["volume", "m³", 10],
// power
0x0228: ["power", "W", 0.001],
0x0229: ["power", "W", 0.01],
0x022a: ["power", "W", 0.1],
0x022b: ["power", "W", 1],
0x022c: ["power", "W", 10],
0x022d: ["power", "W", 100],
0x022e: ["power", "kW", 1],
0x022f: ["power", "kW", 10],
// flow
0x023b: ["flow", "m³/h", 0.001],
0x023c: ["flow", "m³/h", 0.01],
0x023d: ["flow", "m³/h", 0.1],
0x023e: ["flow", "m³/h", 1],
0x023f: ["flow", "m³/h", 10],
// forwardTemperature
0x1258: ["maxForwardTemperature", "°C", 0.001],
0x1259: ["maxForwardTemperature", "°C", 0.01],
0x125a: ["maxForwardTemperature", "°C", 0.1],
0x125b: ["maxForwardTemperature", "°C", 1],
0x0258: ["forwardTemperature", "°C", 0.001],
0x0259: ["forwardTemperature", "°C", 0.01],
0x025a: ["forwardTemperature", "°C", 0.1],
0x025b: ["forwardTemperature", "°C", 1],
// returnTemperature
0x125c: ["maxReturnTemperature", "°C", 0.001],
0x125d: ["maxReturnTemperature", "°C", 0.01],
0x125e: ["maxReturnTemperature", "°C", 0.1],
0x125f: ["maxReturnTemperature", "°C", 1],
0x025c: ["returnTemperature", "°C", 0.001],
0x025d: ["returnTemperature", "°C", 0.01],
0x025e: ["returnTemperature", "°C", 0.1],
0x025f: ["returnTemperature", "°C", 1],
0x07ff: {
0xa0: ["powerFlows", null, null],
},
0x07ff: {
0x21: ["meterInfo", null, null],
},
// bcd values
0x0a58: ["forwardTemperature", "°C", 0.001],
0x0a59: ["forwardTemperature", "°C", 0.01],
0x0a5a: ["forwardTemperature", "°C", 0.1],
0x0a5b: ["forwardTemperature", "°C", 1],
0x0a5c: ["returnTemperature", "°C", 0.001],
0x0a5d: ["returnTemperature", "°C", 0.01],
0x0a5e: ["returnTemperature", "°C", 0.1],
0x0a5f: ["returnTemperature", "°C", 1],
0x0b2b: ["power", "kW", 0.001],
0x0b2c: ["power", "kW", 0.01],
0x0b2d: ["power", "kW", 0.1],
0x0b2e: ["power", "kW", 1],
0x0b3b: ["flow", "m³/h", 0.001],
0x0b3c: ["flow", "m³/h", 0.01],
0x0b3d: ["flow", "m³/h", 0.1],
0x0b3e: ["flow", "m³/h", 1],
0x0c06: ["energy", "kWh", 1],
0x0c07: ["energy", "MWh", 0.01],
0x0cfb: {
0x00: ["energy", "MWh", 0.1],
0x01: ["energy", "MWh", 1],
0x08: ["energy", "GJ", 0.1],
0x09: ["energy", "GJ", 1],
},
0x0c0e: ["energy", "GJ", 0.001],
0x0c0f: ["energy", "GJ", 0.01],
0x0c14: ["volume", "m³", 0.01],
0x0c15: ["volume", "m³", 0.1],
0x0c16: ["volume", "m³", 1],
0x8402: {
0x03: ["tarif2energy", "Wh", 1],
},
0x8403: {
0x03: ["tarif3energy", "Wh", 1],
},
0x8440: {
0x13: ["pulse1volume", "m³", 0.001],
0x14: ["pulse1volume", "m³", 0.01],
0x15: ["pulse1volume", "m³", 0.1],
0x06: ["pulse1energy", "MWh", 0.001],
0x07: ["pulse1energy", "MWh", 0.01],
},
0x8480: {
0x04: {
0x13: ["pulse2volume", "m³", 0.001],
0x14: ["pulse2volume", "m³", 0.01],
0x15: ["pulse2volume", "m³", 0.1],
0x06: ["pulse2energy", "MWh", 0.001],
0x07: ["pulse2energy", "MWh", 0.01],
}
},
0x84c0: {
0x04: {
0x13: ["pulse3volume", "m³", 0.001],
0x14: ["pulse3volume", "m³", 0.01],
0x15: ["pulse3volume", "m³", 0.1],
0x06: ["pulse3energy", "MWh", 0.001],
0x07: ["pulse3energy", "MWh", 0.01],
}
},
0x8c01: {
0x06: ["energyAtDueDate", "kWh", 1]
},
0x8c10: ["tariff1energy", null, 1],
0x8c20: ["tariff2energy", null, 1],
0x9b01: {
0x3b: ["maxFlow", "m³/h", 0.001]
},
0x4c06: ["energy", "kWh", 1],
0x4c07: ["energy", "MWh", 0.01],
0x4cfb: {
0x00: ["energy", "MWh", 0.1],
0x01: ["energy", "MWh", 1],
0x08: ["energy", "GJ", 0.1],
0x09: ["energy", "GJ", 1],
},
0x4c0e: ["energy", "GJ", 0.001],
0x4c0f: ["energy", "GJ", 0.01],
0x4c14: ["volume", "m³", 0.01],
0x4c15: ["volume", "m³", 0.1],
0x4c16: ["volume", "m³", 1],
0xb401: {
0x00: ["previousMonthEnergy", "Wh", 0.001],
0x01: ["previousMonthEnergy", "Wh", 0.01],
0x02: ["previousMonthEnergy", "Wh", 0.1],
0x03: ["previousMonthEnergy", "Wh", 1],
0x04: ["previousMonthEnergy", "Wh", 10],
0x05: ["previousMonthEnergy", "Wh", 100],
0x06: ["previousMonthEnergy", "kWh", 1],
0x07: ["previousMonthEnergy", "kWh", 10],
0x0e: ["previousMonthEnergy", "MJ", 1],
0x0f: ["previousMonthEnergy", "MJ", 10],
},
0x0420: ["S", "s", 1],
0x0421: ["Min", "min", 1],
0x0422: ["H", "h", 1],
0x0423: ["Days", "days", 1],
0xcc10: {
0x07: ["at24tariff1energy", null, 1],
},
0xcc20: {
0x07: ["at24tariff2energy", null, 1],
},
// meterID
0x0c78: ["meterId", null, null],
0x0c79: ["customerMeterId", null, null],
0x06ff: {
0x21: ["meterId", null, null],
},
0x0779: ["enhancedMeterId", null, null],
// error and warning flags
0x01fd: {
0x17: ["errorFlags", null, 1],
},
0x02fd: {
0x17: ["errorFlags", null, 1],
},
0x04fd: {
0x17: ["errorFlags", null, 1],
}
};
function decodeUplink(input) {
let r = Buffer.from(input.bytes);
let pos = 0;
let measurementNo = 0;
let data = {};
let traces = [];
let trace = {};
let debug = input.debug || false;
try {
// Message Format Identifier (1 byte)
data.messageFormat = r.readUInt8(pos); pos++;
const messageFormat = data.messageFormat.toString(16).padStart(2, "0");
const packetInfo = packetTypes[data.messageFormat];
if (typeof (packetInfo) === "undefined") {
throw new Error(`Unsupported message format ${messageFormat}`);
}
data.messageFormatInfo = packetInfo.slice(0, 3);
if (data.messageFormatInfo[1] === "JSON") {
// specific json format
const jsonString = r.toString("utf8", 1);
const measurements = JSON.parse(jsonString);
const unified = {
"messageFormat": data.messageFormat.toString(16).padStart(2, "0"),
"energy": measurements.E,
"energyUnit": measurements.U,
"meterId": measurements.ID,
}
if (debug) {
traces.push(
{
headerStart: 0,
headerInfo: jsonString,
dataStart: 1,
dataEnd: r.length - 2,
}
);
}
return {
data: { ...data, ...unified },
payload: r.toString("hex"),
traces: traces,
warnings: [],
errors: []
};
}
while (pos < (r.length)) {
// read the dib and size
let dif = r.readUInt16BE(pos);
let size = r.readUInt8(pos) & 0xf;
trace = {
headerStart: pos
};
pos += 2;
// check for specific timestamp dib
if (dif == 0x046d || dif == 0x346d) {
trace.dataStart = pos;
const ts = r.subarray(pos, pos + 4); pos += 4;
if (!(ts[1] & 0x01)) {
let year = 1900 + 100 * (ts[1] >> 5);
year += ((ts[3] >> 4) << 3) | (ts[2] >> 5);
data.timeStamp = year + "-" +
("0" + (ts[3] & 0x0f)).substr(-2) + "-" +
("0" + (ts[2] & 0x1f)).substr(-2) + "T" +
("0" + (ts[1] & 0x1f)).substr(-2) + ":" +
("0" + (ts[0] & 0x7f)).substr(-2) + "Z";
}
if (debug) {
traces.push(
{
...trace, ...{
headerInfo: ["timeStamp", null, null],
dataStart: dataStart,
dataEnd: pos - 1,
}
}
);
}
measurementNo++;
continue;
}
let map = valueMap[(dif >> 12) == 0x03 ? dif & 0xfff : dif];
if (typeof (map) === "undefined") {
dif &= 0x0fff;
throw new Error(`Unknown measurement ${dif.toString(16)}`);
}
while (!Array.isArray(map)) {
const subType = r.readUInt8(pos); pos++;
let subMap = map[subType];
if (typeof (subMap) === "undefined") throw new Error(`Unknown measurement ${dif.toString(16)} subtype ${subType.toString(16)}`);
map = subMap;
}
// get default measurement name, or a specific one from the packet definition
let name = (packetInfo.length == 3) ? map[0] : packetInfo[3][measurementNo] + map[0];
// cooling energy can have very special signature 0xff02 and 0xff03
if (name === "coolingEnergy") {
const flag = r.readUInt16LE(pos);
switch (flag) {
case 0xff02: pos += 2; break;
case 0xff03: name = "error" + name; pos += 2; break;
}
}
trace.headerInfo = [name, map[1], map[2]];
trace.dataStart = pos;
// read the value
let value = 0;
switch (size) {
case 0x1: value = r.readUInt8(pos); break;
case 0x2: value = r.readInt16LE(pos); break;
case 0x4: value = r.readInt32LE(pos); break;
case 0x6: {
value = r.readBigInt64BE(pos) & 0xffffffffff; size = 6;
}
case 0x7: {
value = r.readBigInt64BE(pos); size = 8;
} break;
case 0xa: {
const bcd = r.readUInt16LE(pos).toString(16);
value = parseInt(bcd, 10); size = 2;
} break;
case 0xb: {
const bcd = (r.readUInt32LE(pos) & 0xffffff).toString(16);
value = parseInt(bcd, 10); size = 3;
} break;
case 0xc: {
const bcd = r.readUInt32LE(pos).toString(16);
value = parseInt(bcd, 10); size = 4;
} break;
}
pos += size;
// set units
if (map[1] != null) {
data[name + "Unit"] = map[1];
}
// apply multiplier
if (map[2] != null) {
value *= map[2];
}
// set error flag
if ((dif >> 12) == 0x3) {
data[name + "Error"] = true
};
// normalize and store the floating point value
data[name] = (Number.isInteger(value) || typeof (value) === "bigint") ? value : parseFloat(value.toPrecision(8), 10);
measurementNo++;
trace.dataEnd = pos - 1;
if (debug) {
traces.push(trace);
}
}
} catch (e) {
traces.push(trace);
data.messageFormat = data.messageFormat.toString(16).padStart(2, "0"); // change to hex representation
return {
data: data,
payload: r.toString("hex"),
traces: traces,
errors: [e.message],
}
}
data.messageFormat = data.messageFormat.toString(16).padStart(2, "0"); // change to hex representation
return {
data: data,
payload: r.toString("hex"),
traces: traces,
warnings: [],
errors: []
};
}