For Node-RED you might be able to find some libraries to decode M-Bus for you, rather than using a TTN Payload Format to do the preprocessing. On the other hand, you obviously only need the decoding part, not any communication, so any full M-Bus library might be overkill. Also, just in case the device’s format has some errors, you might not be able to fix those. Still then, the JavaScript in NodeJS is a bit more advanced too, and it would keep all your code in one place.
As for the Energy with 0x0CFB00 and 0x0CFB01, I think that indeed adds another byte to the payload to keep 8 digits for the value, as otherwise its existence makes no sense: 6 digits with one or zero decimals would also fit in the other 0x0C06, 0C07, 0C0E and 0C0F formats. So, the “extended VIF” 0xFB seems to be needed for large values. Which also makes me wonder:
So, the unit is not set for some specific output using some configuration? I think it might not be safe to assume the unit and number of decimals never change. If, over time, the values get to be too large to fit in the fixed number of digits, are you sure the device will not decrease its number of decimals, or select a larger unit, to prevent truncation of its readings?
In case you didn’t do that yet, one can easily create a helper for the packed BCD:
/**
* Convert the array of bytes into an unsigned integer, assuming packed
* binary-coded decimal (BCD) with an even number of nibbles, LSB.
*/
function bcdToUint(bytes) {
return bytes.reduceRight(function(acc, byte) {
return 100*acc + 10*(byte >> 4) + (byte & 0x0F);
}, 0);
}
The above can be used with, e.g, Volumen: bcdToUint(bytes.slice(9, 12)) / 100
(where index 12 is not included in the slice of the array, so passes an array of length 3, not 4).
Next, such code is less prone to programming errors when used along with a running index: if some var i
has value 9, then it can be incremented by 3 on the fly using addition assignment, when used in bcdToUint(bytes.slice(i, i += 3))
to indicate one wants to consume the next 3 bytes.
And though the documentation seems to suggest a specific order of the data, each part is also uniquely identified by its “VIF”, and its “DIF” specifies the BCD length. That allows for such something quite generic, also supporting the other formats, like:
/**
* Decoder for M-Bus style payload of the Elvaco CMi4110 sensor.
*
* 2019-11-21: initial, PARTIAL, UNTESTED implementation
*
* Notes:
*
* - In M-Bus the length of BCD values can be determined from the data,
* but the value multiplier (the number of decimals) is part of the
* specification (along with the unit), and not in the data itself.
*
* - The documentation seems to suggest the order of DIBs is fixed, but
* this decoder does not rely on any order, using the VIF values.
*/
/**
* Convert the array of bytes into an unsigned integer, assuming packed
* binary-coded decimal (BCD) with an even number of nibbles, LSB.
*/
function bcdToUint(bytes) {
return bytes.reduceRight(function(acc, byte) {
return 100*acc + 10*(byte >> 4) + (byte & 0x0F);
}, 0);
}
/**
* Get a hexadecimal representation with leading zeroes for each byte.
*/
function hex(bytes, separator) {
return bytes.map(function (b) {
return ('0' + b.toString(16)).substr(-2);
}).join(separator || '');
}
function Decoder(bytes, port) {
var result = {};
var i = 0;
result.messageType = [
'standard',
'compact',
// 0x02: JSON; not supported in this decoder
undefined,
'scheduled daily redundant',
'scheduled extended'
][bytes[i++]];
if (!result.messageType) {
return {
error: 'unsupported message type',
unparsed: hex(bytes)
};
}
// Iterate the M-Bus Data Information Blocks, based on their Data
// Information Field and Value Information Field
while (i < bytes.length) {
var dif = bytes[i++];
var vif = bytes[i++];
var difData = dif & 0x0F;
var isAccumulated = (dif & 0x40) > 0;
// For this device, actually only BCD4, BCD6 and BCD8 are expected
var isBcd = (difData >= 0x09 && difData <= 0x0C) || difData === 0x0E;
var bcdLen = isBcd ? difData & 0x07 : undefined;
// VIF 0xFB is a special case for large values, handled below. For
// other BCD values extract the unsigned integer and increase the
// index here:
var bcdValue = isBcd && vif !== 0xFB
? bcdToUint(bytes.slice(i, i += bcdLen))
: undefined;
// We could switch on just VIF, but then unsupported DIF values
// will go unnoticed
var difVif = dif << 8 | vif;
switch (difVif) {
// Energy, or accumulated energy at 24:00
case 0x0C06:
case 0x0C07:
case 0x4C06:
case 0x4C07:
result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
// 0x06 = 3 decimals, 07 = 2 decimals
value: bcdValue / Math.pow(10, 0x09 - vif),
unit: 'MWh'
};
break;
case 0x0C0E:
case 0x0C0F:
case 0x4C0E:
case 0x4C0F:
result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
// 0x0E = 3 decimals, 0F = 2 decimals
value: bcdValue / Math.pow(10, 0x11 - vif),
unit: 'GJ'
};
break;
case 0x0CFB:
case 0x4CFB:
// Extended VIF, providing an additional byte after VIF
var vifExtension = bytes[i++];
bcdValue = bcdToUint(bytes.slice(i, i += bcdLen));
switch (vifExtension) {
case 0x00:
case 0x01:
result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
// 0x00 = 1 decimal, 01 = no decimals
value: bcdValue / Math.pow(10, 0x01 - vifExtension),
unit: 'MWh'
};
break;
case 0x08:
case 0x09:
result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
// 0x08 = 1 decimal, 09 = no decimals
value: bcdValue / Math.pow(10, 0x09 - vifExtension),
unit: 'GJ'
};
break;
default:
return {
error: 'unexpected extended VIF',
parsed: result,
unparsed: hex(bytes.slice(i - 3))
};
}
break;
// Volume
case 0x0C14:
case 0x0C15:
case 0x0C16:
result.volume = {
// 0x14 = 2 decimals, 15 = 1, 16 = no decimals
value: bcdValue / Math.pow(10, 0x16 - vif),
unit: 'm3'
};
break;
// Power
case 0x0B2B:
case 0x0B2C:
case 0x0B2D:
case 0x0B2E:
result.power = {
// 0x2B = 3 decimals, 2C = 2, 2D = 1, 2E = no decimals
value: bcdValue / Math.pow(10, 0x2E - vif),
unit: 'kW'
};
break;
// Flow
case 0x0B3B:
case 0x0B3C:
case 0x0B3D:
case 0x0B3E:
result.flow = {
// 0x3B = 3 decimals, 3C = 2, 3D = 1, 3E = no decimals
value: bcdValue / Math.pow(10, 0x3E - vif),
unit: 'm3/h'
};
break;
// Forward temperature
case 0x0A5A:
case 0x0A5B:
result.forwardTemperature = {
// 0x5A = 1 decimal, 5B = no decimals
value: bcdValue / Math.pow(10, 0x5B - vif),
unit: 'C'
};
break;
// Return temperature
case 0x0A5E:
case 0x0A5F:
result.returnTemperature = {
// 0x5E = 1 decimal, 5F = no decimals
value: bcdValue / Math.pow(10, 0x5F - vif),
unit: 'C'
};
break;
// Meter ID (using BCD values)
case 0x0C78:
// This will remove leading zeroes in the JSON output
result.meterId = bcdValue;
break;
// Meter date and time
case 0x046D:
// Not really implemented yet
result.dateTime = hex(bytes.slice(i, i += 4));
break;
// Error and warning flags
case 0x02FD:
// Skip the 0x17 in 02FD17 (we should probably validate it)
i++;
result.errorFlags = hex(bytes.slice(i, i += 2));
break;
// Error message
case 0x0E00:
result.error = 'CMi4110 unable to communicate with UH50/UC50';
break;
default:
return {
error: 'unexpected format',
parsed: result,
unparsed: hex(bytes.slice(i - 2))
};
}
}
return result;
}
For the payload from your first post, this yields (sorted for readability):
{
"messageType": "standard",
"energy": {
"unit": "MWh",
"value": 5.857
},
"volume": {
"unit": "m3",
"value": 239.22
},
"power": {
"unit": "kW",
"value": 15.7
},
"flow": {
"unit": "m3/h",
"value": 0.82
},
"forwardTemperature": {
"unit": "C",
"value": 60.6
},
"returnTemperature": {
"unit": "C",
"value": 44.1
},
"meterId": 70183899,
"errorFlags": "0000"
}
Of course: untested… So, future readers: if you ever use this, compare it to the documentation, and please report back here with some real payload values? Thanks.